From f89810a3ade4fd211e02b82c2e8aff67ab715b22 Mon Sep 17 00:00:00 2001 From: Tomasz Iniewicz Date: Mon, 30 Mar 2026 10:39:13 -0400 Subject: [PATCH 1/5] fix: build Agent UI frontend during gaia init and fix doc prerequisites - Extract _ensure_webui_built() to gaia/ui/build.py shared module to avoid circular import risk when init_command.py calls it - Add frontend build step to gaia init (dev/source installs only) - Keep auto-rebuild at gaia --ui launch as staleness safety net - Fix Agent UI docs to clarify Setup -> Quickstart -> Agent UI chain - Add unit tests for all new code paths --- docs/guides/agent-ui.mdx | 32 +++- docs/quickstart.mdx | 4 + src/gaia/cli.py | 93 +----------- src/gaia/installer/init_command.py | 21 ++- src/gaia/ui/build.py | 111 ++++++++++++++ tests/unit/test_webui_build.py | 231 +++++++++++++++++++++++++++++ 6 files changed, 393 insertions(+), 99 deletions(-) create mode 100644 src/gaia/ui/build.py create mode 100644 tests/unit/test_webui_build.py diff --git a/docs/guides/agent-ui.mdx b/docs/guides/agent-ui.mdx index 6464a14b1..49565f482 100644 --- a/docs/guides/agent-ui.mdx +++ b/docs/guides/agent-ui.mdx @@ -6,6 +6,10 @@ icon: "desktop" GAIA Agent UI is a desktop interface for running AI agents **100% locally** on your AMD hardware. Use agents to analyze documents, generate code, answer questions, and accomplish tasks on your PC — all without sending data to the cloud. + + This guide assumes you have completed the [Quickstart](/quickstart) and have GAIA installed. + + **Tested Configuration:** The Agent UI has been tested exclusively on **AMD Ryzen AI MAX+ 395** processors running the **Qwen3.5-35B-A3B-GGUF** model via Lemonade Server. Other hardware or model combinations may work but are not officially verified. @@ -99,6 +103,10 @@ Choose one of the two install paths: The Agent UI starts on [http://localhost:4200](http://localhost:4200). Open this URL in your browser. + + **Source/dev installs:** The frontend is built during `gaia init`. If you see a JSON response instead of the UI, run `gaia init` or manually build with `cd src/gaia/apps/webui && npm install && npm run build`. + + **Options:** | Flag | Description | @@ -166,17 +174,11 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions - ```bash - lemonade-server serve - ``` - - If not installed, run `gaia init --profile minimal` or follow the [Setup Guide](/setup). + Start Lemonade with `lemonade-server serve`. If Lemonade is not installed, follow the initialization steps in the [Quickstart](/quickstart#cli-install). - ```bash - gaia download --agent chat - ``` + Download models with `gaia init --profile chat`. See the [Quickstart](/quickstart#cli-install) for details. @@ -198,6 +200,20 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions - Keep file size under 100MB - For PDF image extraction, download the VLM model: `gaia download --agent chat` + + + The Agent UI frontend has not been built. + + **Source/dev installs (git clone):** Run `gaia init` to build it automatically, or manually: + + ```bash + cd src/gaia/apps/webui && npm install && npm run build + ``` + + Then restart `gaia chat --ui`. + + **pip/PyPI installs:** Use the [npm install path](#install-and-launch) instead — the pip package does not include frontend source files. + --- diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 62c35ab33..cf1729f95 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -4,6 +4,10 @@ description: "Run AI agents locally in minutes, or build your first custom agent icon: "rocket" --- + + This guide has two paths: **Agent UI** (npm, handles everything automatically) and **CLI / Manual Install** (requires [Setup](/setup) prerequisites first). Pick the one that fits your workflow. + + ## Agent UI (Fastest) Run AI agents **100% locally** on your AMD hardware — analyze documents, generate code, answer questions, and accomplish tasks on your PC without sending data to the cloud. diff --git a/src/gaia/cli.py b/src/gaia/cli.py index bad0fd4c8..875e451e0 100644 --- a/src/gaia/cli.py +++ b/src/gaia/cli.py @@ -685,96 +685,11 @@ def run_cli(action, **kwargs): def _ensure_webui_built(log=None): - """Rebuild the Agent UI frontend if source files are newer than dist. + """Rebuild the Agent UI frontend if source files are newer than dist.""" + from gaia.ui.build import ensure_webui_built - Only runs in dev mode (editable install) where the webui src/ directory - exists. Silently skips in installed-package mode or when node/npm are - not available. - """ - if log is None: - log = get_logger(__name__) - - webui_dir = Path(__file__).resolve().parent / "apps" / "webui" - src_dir = webui_dir / "src" - dist_index = webui_dir / "dist" / "index.html" - - # Gate 1 — dev mode only (src/ absent in pip-installed package) - if not src_dir.is_dir(): - return - - # Gate 2 — staleness check - newest_src = 0.0 - for pattern in ("*.ts", "*.tsx", "*.css", "*.html"): - for path in src_dir.rglob(pattern): - mtime = path.stat().st_mtime - if mtime > newest_src: - newest_src = mtime - for root_file in ("index.html", "vite.config.ts", "tsconfig.json"): - p = webui_dir / root_file - if p.exists(): - newest_src = max(newest_src, p.stat().st_mtime) - - if dist_index.exists() and newest_src <= dist_index.stat().st_mtime: - log.debug("Agent UI frontend is up to date") - return - - if dist_index.exists(): - log.info("Agent UI frontend source is newer than built output") - else: - log.info("Agent UI frontend has not been built yet") - - # Gate 3 — node/npm availability - if not shutil.which("node"): - print("Warning: Node.js not found. Cannot auto-rebuild Agent UI frontend.") - print(" The UI may be stale. Install Node.js from https://nodejs.org/") - return - if not shutil.which("npm"): - print("Warning: npm not found. Cannot auto-rebuild Agent UI frontend.") - return - - # On Windows, npm is a .cmd batch file requiring shell execution - _shell = sys.platform == "win32" - - # Step 1 — npm install (only if node_modules/ missing) - if not (webui_dir / "node_modules").is_dir(): - print("Installing Agent UI frontend dependencies...") - try: - subprocess.run( - ["npm", "install"], - cwd=str(webui_dir), - check=True, - capture_output=True, - text=True, - shell=_shell, - ) - except subprocess.CalledProcessError as e: - log.error("npm install failed: %s", e.stderr) - print(f"Warning: npm install failed: {e.stderr}") - print(" Continuing with existing dist/ (may be stale).") - return - except FileNotFoundError: - print("Warning: npm not found. Skipping frontend rebuild.") - return - - # Step 2 — npm run build (stream output so user sees progress) - print("Building Agent UI frontend...", flush=True) - try: - subprocess.run( - ["npm", "run", "build"], - cwd=str(webui_dir), - check=True, - shell=_shell, - ) - print("Agent UI frontend built successfully.") - except subprocess.CalledProcessError as e: - log.error("Frontend build failed (exit code %d)", e.returncode) - print(f"Warning: Frontend build failed (exit code {e.returncode}).") - if dist_index.exists(): - print(" Continuing with existing (possibly stale) build.") - else: - print(" No existing build found. The UI will show a build hint.") - except FileNotFoundError: - print("Warning: npm not found. Skipping frontend rebuild.") + log_fn = log.info if log else print + ensure_webui_built(log_fn=log_fn) def _launch_agent_ui(port=4200, base_url=None, log=None, debug=False): diff --git a/src/gaia/installer/init_command.py b/src/gaia/installer/init_command.py index 55e9088f6..5047d7ee0 100644 --- a/src/gaia/installer/init_command.py +++ b/src/gaia/installer/init_command.py @@ -17,6 +17,7 @@ import subprocess import sys from dataclasses import dataclass +from pathlib import Path from typing import Callable, Optional # Rich imports for better CLI formatting @@ -441,9 +442,14 @@ def run(self) -> int: profile_config = INIT_PROFILES[self.profile] has_pip_extras = bool(profile_config.get("pip_extras")) + _webui_src = Path(__file__).resolve().parent.parent / "apps" / "webui" / "src" + _is_dev_install = _webui_src.is_dir() + total_steps = 4 if not self.skip_models else 3 if has_pip_extras: total_steps += 1 + if _is_dev_install: + total_steps += 1 try: # Step 1: Check/Install Lemonade (skip for remote servers or CI) @@ -502,6 +508,19 @@ def run(self) -> int: ) self._install_pip_extras() + # Build Agent UI frontend (dev/source installs only) + if _is_dev_install: + step_num += 1 + self._print("") + self._print_step(step_num, total_steps, "Building Agent UI frontend...") + try: + from gaia.ui.build import ensure_webui_built + + ensure_webui_built(log_fn=self._print) + self._print_success("Agent UI frontend ready") + except Exception as e: + self._print_warning(f"Frontend build skipped: {e}") + # Final step: Verify setup step_num += 1 self._print("") @@ -1516,8 +1535,6 @@ def _verify_setup(self) -> bool: # Show path for each failed model hf_cache = os.path.expanduser("~/.cache/huggingface/hub") - from pathlib import Path - for model_id, error in models_failed: # Find actual model directory (may have org prefix like ggml-org/model-name) # Search for directories containing the model name diff --git a/src/gaia/ui/build.py b/src/gaia/ui/build.py new file mode 100644 index 000000000..cd4fc71e9 --- /dev/null +++ b/src/gaia/ui/build.py @@ -0,0 +1,111 @@ +# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Shared utility for building the Agent UI frontend. + +Extracted from cli.py so that init_command.py can call it without +creating a circular import through the full CLI module. +""" + +import shutil +import subprocess +import sys +from pathlib import Path + + +def ensure_webui_built(log_fn=print, _webui_dir=None): + """Rebuild the Agent UI frontend if source files are newer than dist. + + Only runs in dev mode (editable install) where the webui src/ directory + exists. Silently skips in installed-package mode or when node/npm are + not available. + + Args: + log_fn: Callable used for user-visible output. Defaults to ``print``. + Pass ``logger.info`` or ``self._print`` to integrate with your + own output mechanism. + _webui_dir: Override the webui directory path (used in tests only). + """ + webui_dir = ( + _webui_dir + if _webui_dir is not None + else Path(__file__).resolve().parent.parent / "apps" / "webui" + ) + src_dir = webui_dir / "src" + dist_index = webui_dir / "dist" / "index.html" + + # Gate 1 — dev mode only (src/ absent in pip-installed package) + if not src_dir.is_dir(): + return + + # Gate 2 — staleness check + newest_src = 0.0 + for pattern in ("*.ts", "*.tsx", "*.css", "*.html"): + for path in src_dir.rglob(pattern): + mtime = path.stat().st_mtime + if mtime > newest_src: + newest_src = mtime + for root_file in ("index.html", "vite.config.ts", "tsconfig.json"): + p = webui_dir / root_file + if p.exists(): + newest_src = max(newest_src, p.stat().st_mtime) + + if dist_index.exists() and newest_src <= dist_index.stat().st_mtime: + return + + if dist_index.exists(): + log_fn("Agent UI frontend source is newer than built output") + else: + log_fn("Agent UI frontend has not been built yet") + + # Gate 3 — node/npm availability + if not shutil.which("node"): + log_fn("Warning: Node.js not found. Cannot auto-rebuild Agent UI frontend.") + log_fn(" The UI may be stale. Install Node.js from https://nodejs.org/") + return + if not shutil.which("npm"): + log_fn("Warning: npm not found. Cannot auto-rebuild Agent UI frontend.") + return + + # On Windows, npm is a .cmd batch file requiring shell execution + _shell = sys.platform == "win32" + + # Step 1 — npm install (only if node_modules/ missing) + if not (webui_dir / "node_modules").is_dir(): + log_fn("Installing Agent UI frontend dependencies...") + try: + subprocess.run( + ["npm", "install"], + cwd=str(webui_dir), + check=True, + capture_output=True, + text=True, + shell=_shell, + ) + except subprocess.CalledProcessError as e: + log_fn(f"Warning: npm install failed: {e.stderr}") + log_fn(" Continuing with existing dist/ (may be stale).") + return + except FileNotFoundError: + log_fn("Warning: npm not found. Skipping frontend rebuild.") + return + + # Step 2 — npm run build (stream output so user sees progress) + log_fn("Building Agent UI frontend...") + try: + subprocess.run( + ["npm", "run", "build"], + cwd=str(webui_dir), + check=True, + shell=_shell, + ) + log_fn("Agent UI frontend built successfully.") + except subprocess.CalledProcessError as e: + log_fn(f"Warning: Frontend build failed (exit code {e.returncode}).") + if dist_index.exists(): + log_fn(" Continuing with existing (possibly stale) build.") + else: + log_fn(" No existing build found. The UI will show a build hint.") + except FileNotFoundError: + log_fn("Warning: npm not found. Skipping frontend rebuild.") diff --git a/tests/unit/test_webui_build.py b/tests/unit/test_webui_build.py new file mode 100644 index 000000000..4fcc057cb --- /dev/null +++ b/tests/unit/test_webui_build.py @@ -0,0 +1,231 @@ +# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Unit tests for gaia.ui.build.ensure_webui_built and the gaia init +frontend build step. + +Tests use real temp directories for path logic and patch only subprocess +and shutil.which so no actual npm/node invocations happen. +""" + +import subprocess +import tempfile +import time +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + + +class TestEnsureWebuiBuilt(unittest.TestCase): + """Tests for gaia.ui.build.ensure_webui_built.""" + + def _call( + self, webui_dir, which_return="/usr/bin/node", run_side_effect=None, log=None + ): + """Helper: call ensure_webui_built with controlled environment.""" + from gaia.ui.build import ensure_webui_built + + msgs = [] + log_fn = log if log is not None else msgs.append + + with ( + patch("gaia.ui.build.shutil.which", return_value=which_return), + patch( + "gaia.ui.build.subprocess.run", + side_effect=run_side_effect, + ) as mock_run, + ): + ensure_webui_built(log_fn=log_fn, _webui_dir=webui_dir) + + return msgs, mock_run + + # ------------------------------------------------------------------ + # Test 1: skip when src/ is absent (pip install, no source tree) + # ------------------------------------------------------------------ + + def test_skips_pip_install(self): + """ensure_webui_built returns early when src/ directory is absent.""" + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + # src/ deliberately NOT created + + msgs, mock_run = self._call(webui_dir) + + mock_run.assert_not_called() + + # ------------------------------------------------------------------ + # Test 2: skip when dist is fresh (staleness check) + # ------------------------------------------------------------------ + + def test_staleness_skip(self): + """No build when dist/index.html is newer than all source files.""" + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + src_dir = webui_dir / "src" + src_dir.mkdir() + # Write a source file with an old mtime + src_file = src_dir / "app.ts" + src_file.write_text("const x = 1;") + + # Create dist/index.html with a NEWER mtime + dist_dir = webui_dir / "dist" + dist_dir.mkdir() + dist_index = dist_dir / "index.html" + dist_index.write_text("") + + # Force dist to appear newer than src + old_time = time.time() - 60 + import os + + os.utime(str(src_file), (old_time, old_time)) + new_time = time.time() + os.utime(str(dist_index), (new_time, new_time)) + + msgs, mock_run = self._call(webui_dir) + + mock_run.assert_not_called() + + # ------------------------------------------------------------------ + # Test 3: node missing — logs warning, no exception + # ------------------------------------------------------------------ + + def test_node_missing(self): + """Log a warning and return gracefully when Node.js is not found.""" + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + (webui_dir / "src").mkdir() + # No dist/index.html — build is needed + + msgs, mock_run = self._call(webui_dir, which_return=None) + + mock_run.assert_not_called() + self.assertTrue( + any("Node.js not found" in m for m in msgs), + f"Expected 'Node.js not found' in log output, got: {msgs}", + ) + + # ------------------------------------------------------------------ + # Test 4: happy path — builds when dist is absent, node/npm available + # ------------------------------------------------------------------ + + def test_builds_frontend(self): + """subprocess.run called with ['npm', 'run', 'build'] when dist absent.""" + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + src_dir = webui_dir / "src" + src_dir.mkdir() + src_file = src_dir / "app.ts" + src_file.write_text("const x = 1;") + # node_modules present so npm install is skipped + (webui_dir / "node_modules").mkdir() + # No dist/index.html — build needed + + msgs, mock_run = self._call(webui_dir) + + called_cmds = [c.args[0] for c in mock_run.call_args_list] + self.assertTrue( + any(c == ["npm", "run", "build"] for c in called_cmds), + f"Expected ['npm', 'run', 'build'] call, got: {called_cmds}", + ) + + # ------------------------------------------------------------------ + # Test 5: npm install failure — no exception propagated + # ------------------------------------------------------------------ + + def test_npm_install_failure_continues(self): + """CalledProcessError from npm install does not propagate.""" + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + (webui_dir / "src").mkdir() + # node_modules absent — triggers npm install + # No dist/index.html + + def fail_install(cmd, **kwargs): + if "install" in cmd: + raise subprocess.CalledProcessError(1, cmd, stderr="ERR") + return MagicMock(returncode=0) + + try: + msgs, mock_run = self._call(webui_dir, run_side_effect=fail_install) + except Exception as e: + self.fail(f"ensure_webui_built raised unexpectedly: {e}") + + +class TestInitCommandWebuiBuild(unittest.TestCase): + """Tests for the gaia init frontend build integration.""" + + def _run_init_with_src_dir_mock(self, src_is_dir: bool): + """ + Run InitCommand.run() with all heavy operations mocked. + + Returns the mock for ensure_webui_built so caller can assert on it. + """ + from gaia.installer.init_command import InitCommand + from gaia.installer.lemonade_installer import LemonadeInstaller + + # Fake src path whose .is_dir() is controlled by the caller + fake_src = MagicMock() + fake_src.is_dir.return_value = src_is_dir + + # Build Path chain via MagicMock's auto-chaining of __truediv__.return_value. + # Path(__file__).resolve().parent.parent / "apps" / "webui" / "src" = fake_src + # Each / uses __truediv__.return_value on the previous mock. + mock_path = MagicMock() + ( + mock_path.return_value.resolve.return_value.parent.parent.__truediv__.return_value.__truediv__.return_value.__truediv__.return_value # / "apps" # / "webui" # / "src" + ) + # Now override the final .return_value to be fake_src + ( + mock_path.return_value.resolve.return_value.parent.parent.__truediv__.return_value.__truediv__.return_value.__truediv__ + ).return_value = fake_src + + mock_installer = MagicMock(spec=LemonadeInstaller) + + with ( + patch("gaia.installer.init_command.Path", mock_path), + patch("gaia.ui.build.ensure_webui_built") as mock_ensure_built, + patch.object(InitCommand, "_print_header"), + patch.object(InitCommand, "_print"), + patch.object(InitCommand, "_print_step"), + patch.object(InitCommand, "_print_success"), + patch.object(InitCommand, "_print_completion"), + patch.object(InitCommand, "_ensure_lemonade_installed", return_value=True), + patch.object(InitCommand, "_ensure_server_running", return_value=True), + patch.object(InitCommand, "_verify_setup", return_value=True), + ): + cmd = InitCommand.__new__(InitCommand) + cmd.profile = "minimal" + cmd.skip_models = True + cmd.skip_lemonade = True + cmd.remote = False + cmd.verbose = False + cmd.force_reinstall = False + cmd._lemonade_base_url = None + cmd.installer = mock_installer + cmd.console = MagicMock() + cmd.run() + + return mock_ensure_built + + # ------------------------------------------------------------------ + # Test 6: init calls ensure_webui_built in dev mode + # ------------------------------------------------------------------ + + def test_init_calls_build_in_dev_mode(self): + """ensure_webui_built is called when webui src/ exists (dev install).""" + mock_ensure_built = self._run_init_with_src_dir_mock(src_is_dir=True) + mock_ensure_built.assert_called_once() + + # ------------------------------------------------------------------ + # Test 7: init skips build for pip installs (no src/) + # ------------------------------------------------------------------ + + def test_init_skips_build_for_pip(self): + """ensure_webui_built is NOT called when webui src/ is absent (pip install).""" + mock_ensure_built = self._run_init_with_src_dir_mock(src_is_dir=False) + mock_ensure_built.assert_not_called() + + +if __name__ == "__main__": + unittest.main() From f3af9b2942dae6dd3818408d6a83161053e91cd0 Mon Sep 17 00:00:00 2001 From: Tomasz Iniewicz Date: Mon, 30 Mar 2026 11:04:05 -0400 Subject: [PATCH 2/5] fix: pass GAIA_WEBUI_DIST env var from gaia-ui to Python server When installed via npm, the Python server now reads GAIA_WEBUI_DIST to locate the pre-built frontend shipped inside the npm package. Falls back to default path for source/dev installs. Closes the JSON error users hit when running gaia-ui after npm install -g @amd-gaia/agent-ui. --- docs/guides/agent-ui.mdx | 4 +- src/gaia/apps/webui/bin/gaia-ui.mjs | 2 +- src/gaia/ui/server.py | 3 +- tests/unit/test_server_webui_dist.py | 86 ++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_server_webui_dist.py diff --git a/docs/guides/agent-ui.mdx b/docs/guides/agent-ui.mdx index 49565f482..0ac00d6aa 100644 --- a/docs/guides/agent-ui.mdx +++ b/docs/guides/agent-ui.mdx @@ -204,6 +204,8 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions The Agent UI frontend has not been built. + **npm install (`gaia-ui`):** This is handled automatically — `gaia-ui` tells the Python server where to find the pre-built frontend. If you still see this error, try reinstalling: `npm install -g @amd-gaia/agent-ui@latest` + **Source/dev installs (git clone):** Run `gaia init` to build it automatically, or manually: ```bash @@ -212,7 +214,7 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions Then restart `gaia chat --ui`. - **pip/PyPI installs:** Use the [npm install path](#install-and-launch) instead — the pip package does not include frontend source files. + **pip/PyPI installs (without gaia-ui):** Use the [npm install path](#install-and-launch) — the pip package does not include frontend source files. diff --git a/src/gaia/apps/webui/bin/gaia-ui.mjs b/src/gaia/apps/webui/bin/gaia-ui.mjs index a881f0821..ff17402eb 100755 --- a/src/gaia/apps/webui/bin/gaia-ui.mjs +++ b/src/gaia/apps/webui/bin/gaia-ui.mjs @@ -371,7 +371,7 @@ function startBackend(gaiaBin, port) { const child = spawn(gaiaBin, ["chat", "--ui", "--ui-port", String(port)], { stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env }, + env: { ...process.env, GAIA_WEBUI_DIST: join(ROOT_DIR, "dist") }, detached: false, }); diff --git a/src/gaia/ui/server.py b/src/gaia/ui/server.py index 6ecfd88c9..d0678e53e 100644 --- a/src/gaia/ui/server.py +++ b/src/gaia/ui/server.py @@ -295,7 +295,8 @@ async def _global_exception_handler(request: Request, exc: Exception): # ── Serve Frontend Static Files ────────────────────────────────────── # Look for built frontend assets in the webui dist directory - _webui_dist = Path(__file__).resolve().parent.parent / "apps" / "webui" / "dist" + _default_dist = Path(__file__).resolve().parent.parent / "apps" / "webui" / "dist" + _webui_dist = Path(os.environ.get("GAIA_WEBUI_DIST", str(_default_dist))) if _webui_dist.is_dir(): logger.info("Serving frontend from %s", _webui_dist) diff --git a/tests/unit/test_server_webui_dist.py b/tests/unit/test_server_webui_dist.py new file mode 100644 index 000000000..b554169a2 --- /dev/null +++ b/tests/unit/test_server_webui_dist.py @@ -0,0 +1,86 @@ +# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +""" +Unit tests verifying that the GAIA Agent UI server reads GAIA_WEBUI_DIST +from the environment to locate the pre-built frontend. + +When installed via npm (`gaia-ui`), the launcher sets this env var so the +Python server can find the dist/ folder inside the npm package rather than +looking in the (absent) PyPI package tree. +""" + +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + + +class TestServerWebuiDist(unittest.TestCase): + """Tests for GAIA_WEBUI_DIST env-var handling in gaia.ui.server.""" + + # ------------------------------------------------------------------ + # Test 1: server reads GAIA_WEBUI_DIST and serves index.html from it + # ------------------------------------------------------------------ + + def test_server_reads_gaia_webui_dist_env_var(self): + """ + When GAIA_WEBUI_DIST points to a real dist dir, create_app() should + register a route for '/' that serves the index.html from that dir. + """ + from fastapi.testclient import TestClient + + with tempfile.TemporaryDirectory() as tmpdir: + dist_dir = Path(tmpdir) + # Create a minimal fake dist layout + (dist_dir / "assets").mkdir() + (dist_dir / "index.html").write_text( + "GAIA Agent UI" + ) + + with patch.dict(os.environ, {"GAIA_WEBUI_DIST": str(dist_dir)}): + # Re-import create_app so the module re-evaluates _webui_dist + import importlib + + import gaia.ui.server as server_mod + + importlib.reload(server_mod) + app = server_mod.create_app(db_path=":memory:") + + client = TestClient(app, raise_server_exceptions=False) + response = client.get("/") + # Should serve the index file (200) rather than the JSON API banner + self.assertEqual(response.status_code, 200) + self.assertIn("GAIA Agent UI", response.text) + + # ------------------------------------------------------------------ + # Test 2: server falls back gracefully when env var is unset + # ------------------------------------------------------------------ + + def test_server_falls_back_to_default_dist_when_env_unset(self): + """ + When GAIA_WEBUI_DIST is not set, create_app() should still succeed. + If the default dist dir doesn't exist the server returns JSON (not a + crash), so we just assert the app is created without raising. + """ + import importlib + + import gaia.ui.server as server_mod + + # Ensure env var is absent + env_without_var = { + k: v for k, v in os.environ.items() if k != "GAIA_WEBUI_DIST" + } + with patch.dict(os.environ, env_without_var, clear=True): + importlib.reload(server_mod) + try: + app = server_mod.create_app(db_path=":memory:") + except Exception as exc: + self.fail(f"create_app() raised unexpectedly when env var unset: {exc}") + + self.assertIsNotNone(app) + + +if __name__ == "__main__": + unittest.main() From b6114a4246fd3bee9c1c1a6e231a6c444b251aeb Mon Sep 17 00:00:00 2001 From: Tomasz Iniewicz Date: Mon, 30 Mar 2026 11:38:25 -0400 Subject: [PATCH 3/5] fix: address code review advisory items A1-A5 - A1: Add warn_fn parameter to ensure_webui_built() so warning/error messages route through log.warning instead of log.info; update cli.py and init_command.py callers accordingly - A2: Add test for npm run build CalledProcessError being caught and returning False - A3: Add test for node-found/npm-missing code path warning and skip - A4: ensure_webui_built() now returns bool (True=success/up-to-date, False=skipped/failed); init only prints "Agent UI frontend ready" when build actually succeeded; _call helper updated to capture and return the bool so all tests verify the contract - A5: Clarify "from the repo root" in manual build commands across two locations in docs/guides/agent-ui.mdx --- docs/guides/agent-ui.mdx | 8 ++- src/gaia/cli.py | 6 +- src/gaia/installer/init_command.py | 7 ++- src/gaia/ui/build.py | 50 +++++++++------ tests/unit/test_webui_build.py | 97 +++++++++++++++++++++++++++--- 5 files changed, 137 insertions(+), 31 deletions(-) diff --git a/docs/guides/agent-ui.mdx b/docs/guides/agent-ui.mdx index 0ac00d6aa..af8dbc6b5 100644 --- a/docs/guides/agent-ui.mdx +++ b/docs/guides/agent-ui.mdx @@ -104,7 +104,11 @@ Choose one of the two install paths: The Agent UI starts on [http://localhost:4200](http://localhost:4200). Open this URL in your browser. - **Source/dev installs:** The frontend is built during `gaia init`. If you see a JSON response instead of the UI, run `gaia init` or manually build with `cd src/gaia/apps/webui && npm install && npm run build`. + **Source/dev installs:** The frontend is built during `gaia init`. If you see a JSON response instead of the UI, run `gaia init` or manually build (from the repo root): + + ```bash + cd src/gaia/apps/webui && npm install && npm run build + ``` **Options:** @@ -206,7 +210,7 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions **npm install (`gaia-ui`):** This is handled automatically — `gaia-ui` tells the Python server where to find the pre-built frontend. If you still see this error, try reinstalling: `npm install -g @amd-gaia/agent-ui@latest` - **Source/dev installs (git clone):** Run `gaia init` to build it automatically, or manually: + **Source/dev installs (git clone):** Run `gaia init` to build it automatically, or manually (from the repo root): ```bash cd src/gaia/apps/webui && npm install && npm run build diff --git a/src/gaia/cli.py b/src/gaia/cli.py index 875e451e0..c33a07968 100644 --- a/src/gaia/cli.py +++ b/src/gaia/cli.py @@ -688,8 +688,10 @@ def _ensure_webui_built(log=None): """Rebuild the Agent UI frontend if source files are newer than dist.""" from gaia.ui.build import ensure_webui_built - log_fn = log.info if log else print - ensure_webui_built(log_fn=log_fn) + ensure_webui_built( + log_fn=log.info if log else print, + warn_fn=log.warning if log else print, + ) def _launch_agent_ui(port=4200, base_url=None, log=None, debug=False): diff --git a/src/gaia/installer/init_command.py b/src/gaia/installer/init_command.py index 5047d7ee0..db02d80e7 100644 --- a/src/gaia/installer/init_command.py +++ b/src/gaia/installer/init_command.py @@ -516,8 +516,11 @@ def run(self) -> int: try: from gaia.ui.build import ensure_webui_built - ensure_webui_built(log_fn=self._print) - self._print_success("Agent UI frontend ready") + built = ensure_webui_built( + log_fn=self._print, warn_fn=self._print_warning + ) + if built: + self._print_success("Agent UI frontend ready") except Exception as e: self._print_warning(f"Frontend build skipped: {e}") diff --git a/src/gaia/ui/build.py b/src/gaia/ui/build.py index cd4fc71e9..9a68d0855 100644 --- a/src/gaia/ui/build.py +++ b/src/gaia/ui/build.py @@ -14,7 +14,7 @@ from pathlib import Path -def ensure_webui_built(log_fn=print, _webui_dir=None): +def ensure_webui_built(log_fn=print, warn_fn=None, _webui_dir=None): """Rebuild the Agent UI frontend if source files are newer than dist. Only runs in dev mode (editable install) where the webui src/ directory @@ -22,11 +22,22 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): not available. Args: - log_fn: Callable used for user-visible output. Defaults to ``print``. + log_fn: Callable used for informational output. Defaults to ``print``. Pass ``logger.info`` or ``self._print`` to integrate with your own output mechanism. + warn_fn: Callable used for warning/error output. Defaults to + ``log_fn`` when not provided. Pass ``logger.warning`` or + ``self._print_warning`` to route warnings separately from + informational messages. _webui_dir: Override the webui directory path (used in tests only). + + Returns: + True — build succeeded, or dist is already up-to-date. + False — build was skipped or failed (warn_fn already reported why). """ + if warn_fn is None: + warn_fn = log_fn + webui_dir = ( _webui_dir if _webui_dir is not None @@ -37,7 +48,7 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): # Gate 1 — dev mode only (src/ absent in pip-installed package) if not src_dir.is_dir(): - return + return False # Gate 2 — staleness check newest_src = 0.0 @@ -52,7 +63,7 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): newest_src = max(newest_src, p.stat().st_mtime) if dist_index.exists() and newest_src <= dist_index.stat().st_mtime: - return + return True if dist_index.exists(): log_fn("Agent UI frontend source is newer than built output") @@ -61,12 +72,12 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): # Gate 3 — node/npm availability if not shutil.which("node"): - log_fn("Warning: Node.js not found. Cannot auto-rebuild Agent UI frontend.") - log_fn(" The UI may be stale. Install Node.js from https://nodejs.org/") - return + warn_fn("Warning: Node.js not found. Cannot auto-rebuild Agent UI frontend.") + warn_fn(" The UI may be stale. Install Node.js from https://nodejs.org/") + return False if not shutil.which("npm"): - log_fn("Warning: npm not found. Cannot auto-rebuild Agent UI frontend.") - return + warn_fn("Warning: npm not found. Cannot auto-rebuild Agent UI frontend.") + return False # On Windows, npm is a .cmd batch file requiring shell execution _shell = sys.platform == "win32" @@ -84,12 +95,12 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): shell=_shell, ) except subprocess.CalledProcessError as e: - log_fn(f"Warning: npm install failed: {e.stderr}") - log_fn(" Continuing with existing dist/ (may be stale).") - return + warn_fn(f"Warning: npm install failed: {e.stderr}") + warn_fn(" Continuing with existing dist/ (may be stale).") + return False except FileNotFoundError: - log_fn("Warning: npm not found. Skipping frontend rebuild.") - return + warn_fn("Warning: npm not found. Skipping frontend rebuild.") + return False # Step 2 — npm run build (stream output so user sees progress) log_fn("Building Agent UI frontend...") @@ -101,11 +112,14 @@ def ensure_webui_built(log_fn=print, _webui_dir=None): shell=_shell, ) log_fn("Agent UI frontend built successfully.") + return True except subprocess.CalledProcessError as e: - log_fn(f"Warning: Frontend build failed (exit code {e.returncode}).") + warn_fn(f"Warning: Frontend build failed (exit code {e.returncode}).") if dist_index.exists(): - log_fn(" Continuing with existing (possibly stale) build.") + warn_fn(" Continuing with existing (possibly stale) build.") else: - log_fn(" No existing build found. The UI will show a build hint.") + warn_fn(" No existing build found. The UI will show a build hint.") + return False except FileNotFoundError: - log_fn("Warning: npm not found. Skipping frontend rebuild.") + warn_fn("Warning: npm not found. Skipping frontend rebuild.") + return False diff --git a/tests/unit/test_webui_build.py b/tests/unit/test_webui_build.py index 4fcc057cb..cc9e96a23 100644 --- a/tests/unit/test_webui_build.py +++ b/tests/unit/test_webui_build.py @@ -36,9 +36,9 @@ def _call( side_effect=run_side_effect, ) as mock_run, ): - ensure_webui_built(log_fn=log_fn, _webui_dir=webui_dir) + result = ensure_webui_built(log_fn=log_fn, _webui_dir=webui_dir) - return msgs, mock_run + return msgs, mock_run, result # ------------------------------------------------------------------ # Test 1: skip when src/ is absent (pip install, no source tree) @@ -50,9 +50,10 @@ def test_skips_pip_install(self): webui_dir = Path(tmpdir) # src/ deliberately NOT created - msgs, mock_run = self._call(webui_dir) + msgs, mock_run, result = self._call(webui_dir) mock_run.assert_not_called() + self.assertFalse(result, "Expected False (silent skip) when src/ is absent") # ------------------------------------------------------------------ # Test 2: skip when dist is fresh (staleness check) @@ -82,9 +83,10 @@ def test_staleness_skip(self): new_time = time.time() os.utime(str(dist_index), (new_time, new_time)) - msgs, mock_run = self._call(webui_dir) + msgs, mock_run, result = self._call(webui_dir) mock_run.assert_not_called() + self.assertTrue(result, "Expected True when dist is already up-to-date") # ------------------------------------------------------------------ # Test 3: node missing — logs warning, no exception @@ -97,9 +99,10 @@ def test_node_missing(self): (webui_dir / "src").mkdir() # No dist/index.html — build is needed - msgs, mock_run = self._call(webui_dir, which_return=None) + msgs, mock_run, result = self._call(webui_dir, which_return=None) mock_run.assert_not_called() + self.assertFalse(result, "Expected False when Node.js is missing") self.assertTrue( any("Node.js not found" in m for m in msgs), f"Expected 'Node.js not found' in log output, got: {msgs}", @@ -121,13 +124,14 @@ def test_builds_frontend(self): (webui_dir / "node_modules").mkdir() # No dist/index.html — build needed - msgs, mock_run = self._call(webui_dir) + msgs, mock_run, result = self._call(webui_dir) called_cmds = [c.args[0] for c in mock_run.call_args_list] self.assertTrue( any(c == ["npm", "run", "build"] for c in called_cmds), f"Expected ['npm', 'run', 'build'] call, got: {called_cmds}", ) + self.assertTrue(result, "Expected True when build succeeds") # ------------------------------------------------------------------ # Test 5: npm install failure — no exception propagated @@ -147,10 +151,89 @@ def fail_install(cmd, **kwargs): return MagicMock(returncode=0) try: - msgs, mock_run = self._call(webui_dir, run_side_effect=fail_install) + msgs, mock_run, result = self._call( + webui_dir, run_side_effect=fail_install + ) except Exception as e: self.fail(f"ensure_webui_built raised unexpectedly: {e}") + self.assertFalse(result, "Expected False when npm install fails") + + # ------------------------------------------------------------------ + # Test 6: npm run build failure — caught, returns False, no exception + # ------------------------------------------------------------------ + + def test_build_step_failure_continues(self): + """npm run build CalledProcessError is caught; returns False without raising.""" + from gaia.ui.build import ensure_webui_built + + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + (webui_dir / "src").mkdir() + # node_modules present so npm install is skipped + (webui_dir / "node_modules").mkdir() + # No dist/index.html — build is needed + + def fail_build(cmd, **kwargs): + if "build" in cmd: + raise subprocess.CalledProcessError(1, cmd) + return MagicMock(returncode=0) + + warnings = [] + result = None + try: + with ( + patch("gaia.ui.build.shutil.which", return_value="/usr/bin/node"), + patch("gaia.ui.build.subprocess.run", side_effect=fail_build), + ): + result = ensure_webui_built( + _webui_dir=webui_dir, warn_fn=warnings.append + ) + except Exception as e: + self.fail(f"ensure_webui_built raised unexpectedly: {e}") + + self.assertFalse(result, "Expected False when build step fails") + self.assertTrue( + any( + "build failed" in w.lower() or "Frontend build failed" in w + for w in warnings + ), + f"Expected build-failure warning, got: {warnings}", + ) + + # ------------------------------------------------------------------ + # Test 7: node found, npm missing — warns and skips build + # ------------------------------------------------------------------ + + def test_npm_missing_warns_and_skips(self): + """If node is present but npm is missing, warn_fn is called and build is skipped.""" + from gaia.ui.build import ensure_webui_built + + with tempfile.TemporaryDirectory() as tmpdir: + webui_dir = Path(tmpdir) + (webui_dir / "src").mkdir() + # No dist/index.html — build would be needed + + warnings = [] + + def fake_which(cmd): + return "/usr/bin/node" if cmd == "node" else None + + with ( + patch("gaia.ui.build.shutil.which", side_effect=fake_which), + patch("gaia.ui.build.subprocess.run") as mock_run, + ): + result = ensure_webui_built( + _webui_dir=webui_dir, warn_fn=warnings.append + ) + + mock_run.assert_not_called() + self.assertFalse(result, "Expected False when npm is missing") + self.assertTrue( + any("npm" in w.lower() for w in warnings), + f"Expected npm warning in warn_fn output, got: {warnings}", + ) + class TestInitCommandWebuiBuild(unittest.TestCase): """Tests for the gaia init frontend build integration.""" From 1fa20969a3b1e11f3169fcfbe1d5918a6d9ec4df Mon Sep 17 00:00:00 2001 From: Tomasz Iniewicz Date: Mon, 30 Mar 2026 13:25:45 -0400 Subject: [PATCH 4/5] fix: replace GAIA_WEBUI_DIST env var with --ui-dist CLI argument Replace the environment variable mechanism for specifying the frontend dist directory with an explicit --ui-dist CLI argument. This avoids implicit env var coupling between gaia-ui and the Python server. - Add --ui-dist to top-level and chat subcommand parsers in cli.py - Add webui_dist parameter to _launch_agent_ui() and create_app() - Pass --ui-dist via spawn args in gaia-ui.mjs instead of env override - Add --ui-dist to standalone server.py main() runner - Clarify per-scenario restart commands in agent-ui.mdx accordion - Rewrite test to use webui_dist parameter directly (no module reload) --- docs/guides/agent-ui.mdx | 2 +- src/gaia/apps/webui/bin/gaia-ui.mjs | 3 +- src/gaia/cli.py | 17 +++++++- src/gaia/ui/server.py | 13 ++++-- tests/unit/test_server_webui_dist.py | 59 ++++++---------------------- 5 files changed, 38 insertions(+), 56 deletions(-) diff --git a/docs/guides/agent-ui.mdx b/docs/guides/agent-ui.mdx index af8dbc6b5..58f0ae0c5 100644 --- a/docs/guides/agent-ui.mdx +++ b/docs/guides/agent-ui.mdx @@ -208,7 +208,7 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions The Agent UI frontend has not been built. - **npm install (`gaia-ui`):** This is handled automatically — `gaia-ui` tells the Python server where to find the pre-built frontend. If you still see this error, try reinstalling: `npm install -g @amd-gaia/agent-ui@latest` + **npm install (`gaia-ui`):** This is handled automatically — `gaia-ui` tells the Python server where to find the pre-built frontend. If you still see this error, try reinstalling: `npm install -g @amd-gaia/agent-ui@latest` Then restart with `gaia-ui`. **Source/dev installs (git clone):** Run `gaia init` to build it automatically, or manually (from the repo root): diff --git a/src/gaia/apps/webui/bin/gaia-ui.mjs b/src/gaia/apps/webui/bin/gaia-ui.mjs index ff17402eb..14db191b9 100755 --- a/src/gaia/apps/webui/bin/gaia-ui.mjs +++ b/src/gaia/apps/webui/bin/gaia-ui.mjs @@ -369,9 +369,8 @@ function openBrowser(url) { function startBackend(gaiaBin, port) { console.log(`Starting GAIA backend on port ${port}...`); - const child = spawn(gaiaBin, ["chat", "--ui", "--ui-port", String(port)], { + const child = spawn(gaiaBin, ["chat", "--ui", "--ui-port", String(port), "--ui-dist", join(ROOT_DIR, "dist")], { stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, GAIA_WEBUI_DIST: join(ROOT_DIR, "dist") }, detached: false, }); diff --git a/src/gaia/cli.py b/src/gaia/cli.py index c33a07968..787b698da 100644 --- a/src/gaia/cli.py +++ b/src/gaia/cli.py @@ -694,7 +694,7 @@ def _ensure_webui_built(log=None): ) -def _launch_agent_ui(port=4200, base_url=None, log=None, debug=False): +def _launch_agent_ui(port=4200, base_url=None, log=None, debug=False, webui_dist=None): """Launch the Agent UI server (FastAPI + uvicorn). Reused by top-level --ui, gaia chat --ui, and the interactive menu. @@ -728,7 +728,7 @@ def _launch_agent_ui(port=4200, base_url=None, log=None, debug=False): import uvicorn - app = create_app() + app = create_app(webui_dist=webui_dist) uvicorn.run( app, host="127.0.0.1", @@ -884,6 +884,11 @@ def main(): default=4200, help="Port for the Agent UI server (default: 4200, used with --ui)", ) + parser.add_argument( + "--ui-dist", + default=None, + help="Path to pre-built Agent UI frontend dist directory (used with --ui)", + ) parser.add_argument( "--cli", action="store_true", @@ -1056,6 +1061,11 @@ def main(): default=4200, help="Port for the Agent UI server (default: 4200)", ) + chat_parser.add_argument( + "--ui-dist", + default=None, + help="Path to pre-built Agent UI frontend dist directory (used with --ui)", + ) talk_parser = subparsers.add_parser( "talk", help="Start voice conversation with Gaia", parents=[parent_parser] ) @@ -2627,6 +2637,7 @@ def main(): base_url=getattr(args, "base_url", None), log=log, debug=getattr(args, "debug", False), + webui_dist=getattr(args, "ui_dist", None), ) return @@ -2641,6 +2652,7 @@ def main(): base_url=getattr(args, "base_url", None), log=log, debug=getattr(args, "debug", False), + webui_dist=getattr(args, "ui_dist", None), ) return @@ -2660,6 +2672,7 @@ def main(): base_url=getattr(args, "base_url", None), log=log, debug=getattr(args, "debug", False), + webui_dist=getattr(args, "ui_dist", None), ) return diff --git a/src/gaia/ui/server.py b/src/gaia/ui/server.py index d0678e53e..97076445e 100644 --- a/src/gaia/ui/server.py +++ b/src/gaia/ui/server.py @@ -127,11 +127,13 @@ async def dispatch(self, request: Request, call_next): # ── Application Factory ──────────────────────────────────────────────────── -def create_app(db_path: str = None) -> FastAPI: +def create_app(db_path: str = None, webui_dist: str = None) -> FastAPI: """Create and configure the FastAPI application. Args: db_path: Path to SQLite database. None for default, ":memory:" for testing. + webui_dist: Path to the pre-built frontend dist directory. When None, + falls back to the default location relative to this package. Returns: Configured FastAPI application. @@ -296,7 +298,7 @@ async def _global_exception_handler(request: Request, exc: Exception): # ── Serve Frontend Static Files ────────────────────────────────────── # Look for built frontend assets in the webui dist directory _default_dist = Path(__file__).resolve().parent.parent / "apps" / "webui" / "dist" - _webui_dist = Path(os.environ.get("GAIA_WEBUI_DIST", str(_default_dist))) + _webui_dist = Path(webui_dist) if webui_dist else _default_dist if _webui_dist.is_dir(): logger.info("Serving frontend from %s", _webui_dist) @@ -376,11 +378,16 @@ def main(): "--port", type=int, default=DEFAULT_PORT, help=f"Port (default: {DEFAULT_PORT})" ) parser.add_argument("--debug", action="store_true", help="Enable debug logging") + parser.add_argument( + "--ui-dist", + default=None, + help="Path to pre-built Agent UI frontend dist directory", + ) args = parser.parse_args() log_level = "debug" if args.debug else "info" print(f"Starting GAIA Agent UI server on http://{args.host}:{args.port}") - server_app = create_app() + server_app = create_app(webui_dist=args.ui_dist) uvicorn.run( server_app, host=args.host, diff --git a/tests/unit/test_server_webui_dist.py b/tests/unit/test_server_webui_dist.py index b554169a2..bbc048a76 100644 --- a/tests/unit/test_server_webui_dist.py +++ b/tests/unit/test_server_webui_dist.py @@ -2,85 +2,48 @@ # SPDX-License-Identifier: MIT """ -Unit tests verifying that the GAIA Agent UI server reads GAIA_WEBUI_DIST -from the environment to locate the pre-built frontend. +Unit tests verifying that the GAIA Agent UI server uses the webui_dist +parameter of create_app() to locate the pre-built frontend. -When installed via npm (`gaia-ui`), the launcher sets this env var so the +When installed via npm (`gaia-ui`), the launcher passes --ui-dist so the Python server can find the dist/ folder inside the npm package rather than looking in the (absent) PyPI package tree. """ -import os import tempfile import unittest from pathlib import Path -from unittest.mock import patch class TestServerWebuiDist(unittest.TestCase): - """Tests for GAIA_WEBUI_DIST env-var handling in gaia.ui.server.""" + """Tests for webui_dist parameter handling in gaia.ui.server.create_app().""" # ------------------------------------------------------------------ - # Test 1: server reads GAIA_WEBUI_DIST and serves index.html from it + # Test 1: server uses webui_dist parameter and serves index.html from it # ------------------------------------------------------------------ - def test_server_reads_gaia_webui_dist_env_var(self): + def test_server_uses_provided_webui_dist(self): """ - When GAIA_WEBUI_DIST points to a real dist dir, create_app() should - register a route for '/' that serves the index.html from that dir. + When webui_dist is passed to create_app(), the server serves index.html + from that directory instead of returning the JSON API banner. """ from fastapi.testclient import TestClient + from gaia.ui.server import create_app + with tempfile.TemporaryDirectory() as tmpdir: dist_dir = Path(tmpdir) - # Create a minimal fake dist layout (dist_dir / "assets").mkdir() (dist_dir / "index.html").write_text( "GAIA Agent UI" ) - with patch.dict(os.environ, {"GAIA_WEBUI_DIST": str(dist_dir)}): - # Re-import create_app so the module re-evaluates _webui_dist - import importlib - - import gaia.ui.server as server_mod - - importlib.reload(server_mod) - app = server_mod.create_app(db_path=":memory:") - + app = create_app(webui_dist=str(dist_dir), db_path=":memory:") client = TestClient(app, raise_server_exceptions=False) response = client.get("/") - # Should serve the index file (200) rather than the JSON API banner self.assertEqual(response.status_code, 200) self.assertIn("GAIA Agent UI", response.text) - # ------------------------------------------------------------------ - # Test 2: server falls back gracefully when env var is unset - # ------------------------------------------------------------------ - - def test_server_falls_back_to_default_dist_when_env_unset(self): - """ - When GAIA_WEBUI_DIST is not set, create_app() should still succeed. - If the default dist dir doesn't exist the server returns JSON (not a - crash), so we just assert the app is created without raising. - """ - import importlib - - import gaia.ui.server as server_mod - - # Ensure env var is absent - env_without_var = { - k: v for k, v in os.environ.items() if k != "GAIA_WEBUI_DIST" - } - with patch.dict(os.environ, env_without_var, clear=True): - importlib.reload(server_mod) - try: - app = server_mod.create_app(db_path=":memory:") - except Exception as exc: - self.fail(f"create_app() raised unexpectedly when env var unset: {exc}") - - self.assertIsNotNone(app) - if __name__ == "__main__": unittest.main() From 5746ce3dbeabfbefb7e62160fca19688c213df00 Mon Sep 17 00:00:00 2001 From: Tomasz Iniewicz Date: Mon, 30 Mar 2026 20:57:16 -0400 Subject: [PATCH 5/5] release: GAIA v0.17.1 --- docs/docs.json | 3 +- docs/releases/v0.17.1.mdx | 83 ++++++++++++++++++++++++++++++++ src/gaia/apps/webui/package.json | 2 +- src/gaia/version.py | 2 +- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 docs/releases/v0.17.1.mdx diff --git a/docs/docs.json b/docs/docs.json index ed30fdccd..b72e559c8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -370,6 +370,7 @@ "group": "Release Notes", "pages": [ "releases/index", + "releases/v0.17.1", "releases/v0.17.0", "releases/v0.16.1", "releases/v0.16.0", @@ -414,7 +415,7 @@ "navbar": { "links": [ { - "label": "v0.17.0 \u00b7 Lemonade 10.0.0", + "label": "v0.17.1 \u00b7 Lemonade 10.0.0", "href": "https://github.com/amd/gaia/releases" }, { diff --git a/docs/releases/v0.17.1.mdx b/docs/releases/v0.17.1.mdx new file mode 100644 index 000000000..c70051db7 --- /dev/null +++ b/docs/releases/v0.17.1.mdx @@ -0,0 +1,83 @@ +--- +title: "v0.17.1" +description: "Agent UI works out of the box — automatic frontend build on gaia init, no manual npm steps" +--- + +# GAIA v0.17.1 Release Notes + +Agent UI now works from the moment you finish `gaia init` — no manual frontend build steps, no hunting for the right `npm` command. This patch release makes the first-install experience reliable and cleans up how you point to a custom frontend dist directory. + +```bash +npm install -g @amd-gaia/agent-ui +gaia-ui +``` + +**Why upgrade:** +- **Agent UI works immediately after `gaia init`** — The frontend build now runs automatically during initialization on dev/source installs, so `gaia chat --ui` is ready to go without any extra steps +- **Cleaner configuration with `--ui-dist`** — A new CLI flag replaces the opaque `GAIA_WEBUI_DIST` environment variable, making it obvious how to point to a custom frontend dist directory +- **More reliable publishing pipeline** — The `@amd-gaia/agent-ui` npm package now publishes via OIDC trusted publishing, eliminating NPM token rotation issues that could delay releases + +Get started with the [Agent UI guide](/guides/agent-ui). + +--- + +## What's New + +### Agent UI First-Install Reliability + +Previously, after running `gaia init` on a dev or source install, launching `gaia chat --ui` would fail because the React frontend hadn't been built yet. Users had to manually run `cd src/gaia/apps/webui && npm install && npm run build` — a step easy to miss and not clearly documented (commit f89810a3). + +**What you can do:** +- Run `gaia init` once and immediately launch `gaia chat --ui` — the frontend is built automatically +- Skip the manual build step entirely on dev/source installs + +**Under the hood:** +- Extracted `_ensure_webui_built()` into a shared `gaia/ui/build.py` module used by both `gaia init` and `gaia chat --ui` +- `gaia chat --ui` retains its staleness check and auto-rebuild as a safety net +- Added unit tests for all new build code paths +- Fixed Agent UI docs to clarify the Setup → Quickstart → Agent UI prerequisite chain + +--- + +### `--ui-dist` Configuration Flag + +The `GAIA_WEBUI_DIST` environment variable for pointing to a custom frontend dist directory has been replaced with a `--ui-dist` CLI argument (commits f3af9b29, 1fa20969). This follows GAIA's CLI-first configuration pattern and makes the option discoverable via `--help`. + +**What you can do:** +- `gaia chat --ui --ui-dist /path/to/dist` — point to a custom frontend build +- `gaia-ui` npm launcher passes this automatically — no manual env var setup needed + +**Under the hood:** +- `--ui-dist` accepted by both `gaia chat --ui` and the UI server directly +- Code review advisory items A1–A5 addressed (commit b6114a42) + +--- + +## Infrastructure + +- **OIDC trusted publishing** — `@amd-gaia/agent-ui` npm package now publishes via OIDC, removing the NPM_TOKEN secret requirement and making the publishing pipeline more reliable (PRs #638, #639) +- **Merge queue fix** — Resolved phantom failures in the merge-queue-notify CI workflow (PR #640) + +--- + +## Upgrade + +```bash +npm install -g @amd-gaia/agent-ui@latest +``` + +--- + +## Full Changelog + +**7 commits** since v0.17.0: + +- `1fa20969` - fix: replace GAIA_WEBUI_DIST env var with --ui-dist CLI argument +- `b6114a42` - fix: address code review advisory items A1-A5 +- `f3af9b29` - fix: pass GAIA_WEBUI_DIST env var from gaia-ui to Python server +- `f89810a3` - fix: build Agent UI frontend during gaia init and fix doc prerequisites +- `334b011c` - fix: remove registry-url to enable OIDC trusted publishing (#639) +- `776dc34a` - fix: resolve merge-queue-notify phantom failures (#640) +- `83a4db1b` - fix: switch npm publish to OIDC trusted publishing (#638) + +Full Changelog: [v0.17.0...v0.17.1](https://github.com/amd/gaia/compare/v0.17.0...v0.17.1) diff --git a/src/gaia/apps/webui/package.json b/src/gaia/apps/webui/package.json index 4bf054d8d..ea9499aa8 100644 --- a/src/gaia/apps/webui/package.json +++ b/src/gaia/apps/webui/package.json @@ -1,6 +1,6 @@ { "name": "@amd-gaia/agent-ui", - "version": "0.17.1-rc.1", + "version": "0.17.1", "type": "module", "productName": "GAIA Agent UI", "description": "Privacy-first agentic AI interface with document Q&A - runs 100% locally on AMD Ryzen AI", diff --git a/src/gaia/version.py b/src/gaia/version.py index ae3291e33..e5e24ec15 100644 --- a/src/gaia/version.py +++ b/src/gaia/version.py @@ -6,7 +6,7 @@ import subprocess from importlib.metadata import version as get_package_version_metadata -__version__ = "0.17.1-rc.1" +__version__ = "0.17.1" # Lemonade version used across CI and installer LEMONADE_VERSION = "10.0.0"