diff --git a/.gitignore b/.gitignore index 3846911e..b4ee8a54 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ docs/* !docs/ui-ux/** !docs/docker.md !docs/supervisor.md +!docs/troubleshooting.md # Local-only PR review harness: rendering drivers, sample bank, fixtures. # Used by Claude during deep reviews; never shared in the repo. diff --git a/CHANGELOG.md b/CHANGELOG.md index 916b0d29..303859af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Hermes Web UI -- Changelog +## [v0.51.7] — 2026-05-05 — single-PR docs+dx (#1695) + +### Changed + +- **#1695 — better diagnostic on `AIAgent not available` (DX + docs).** When the WebUI was launched with a Python that can't import `run_agent.AIAgent`, every chat request raised a bare `ImportError("AIAgent not available -- check that hermes-agent is on sys.path")` with no information about which Python was running, where it was looking, or what to do next. @Patrick-81 reported the symptom on a symlinked install (#1695); the maintainer's response (which Patrick confirmed worked) was a three-step diagnostic flow that we've now baked into the error message itself plus a new `docs/troubleshooting.md`. The error now includes: the running Python interpreter, the `HERMES_WEBUI_AGENT_DIR` env (set vs not set), the relevant `sys.path` entries (those mentioning hermes/agent), the most-common fix (`pip install -e .` in the agent dir), and a pointer to `docs/troubleshooting.md`. Docs entry walks through `ls`/`readlink`/`pip install -e .` diagnostic steps, three common failure modes (not on sys.path, broken symlink, wrong override), and when to file a bug. + +### Added + +- **`docs/troubleshooting.md`** — new diagnostic-flow doc with one entry to start (`AIAgent not available`); structured as Symptom → Why → Diagnostic commands → Fix → When to file a bug. Linked from README's `## Docs` section. Future failure-mode entries follow the same template. + ## [v0.51.6] — 2026-05-05 — 5-PR full-sweep batch ### Added diff --git a/README.md b/README.md index d2b9edd9..810b9eaf 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,7 @@ State lives outside the repo at `~/.hermes/webui-mvp/` by default - `CHANGELOG.md` -- release notes per sprint - `SPRINTS.md` -- forward sprint plan with CLI + Claude parity targets - `THEMES.md` -- theme system documentation, custom theme guide +- `docs/troubleshooting.md` -- diagnostic flows for common failures (e.g. "AIAgent not available") ## Contributors diff --git a/ROADMAP.md b/ROADMAP.md index 82f00296..1f108cfb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ > Web companion to the Hermes Agent CLI. Same workflows, browser-native. > -> Last updated: v0.51.6 (May 5, 2026) — 4537 tests collected +> Last updated: v0.51.7 (May 5, 2026) — 4550 tests collected — single-PR docs+dx (#1695: AIAgent not available diagnostic) > Test source: `pytest tests/ --collect-only -q` > Per-version detail: see [CHANGELOG.md](./CHANGELOG.md) diff --git a/TESTING.md b/TESTING.md index c13172d6..48cbd5a5 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1835,7 +1835,7 @@ Bridged CLI sessions: --- -*Last updated: v0.51.6, May 5, 2026* +*Last updated: v0.51.7, May 5, 2026* *Total automated tests collected: 4537* *Regression gate: tests/test_regressions.py* *Run: pytest tests/ -v --timeout=60* diff --git a/api/streaming.py b/api/streaming.py index e129186b..8bed63dc 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -59,6 +59,55 @@ def _get_ai_agent(): except ImportError: pass return AIAgent + + +def _aiagent_import_error_detail() -> str: + """Return a multi-line diagnostic string for the "AIAgent not available" path. + + The bare ImportError ("AIAgent not available -- check that hermes-agent is + on sys.path") leaves users guessing at which python is running, where it's + looking, and what to fix. We assemble the same evidence a maintainer would + ask for first (issue #1695): the python that's running, the agent_dir env + var if set, the sys.path entries that mention 'hermes', and the most-common + fix (`pip install -e .` in the agent dir). + + Kept as a separate helper so it stays out of the hot path until we actually + need to raise — building it on every successful import would be wasted work. + """ + import os as _os + import sys as _sys + + lines = ["AIAgent not available -- check that hermes-agent is on sys.path"] + lines.append("") + lines.append(f" python: {_sys.executable}") + agent_dir = _os.environ.get("HERMES_WEBUI_AGENT_DIR") + if agent_dir: + lines.append(f" HERMES_WEBUI_AGENT_DIR: {agent_dir}") + else: + lines.append(" HERMES_WEBUI_AGENT_DIR: (not set)") + + # Show only the sys.path entries that look relevant — full sys.path is noisy. + relevant = [p for p in _sys.path if "hermes" in p.lower() or "agent" in p.lower()] + if relevant: + lines.append(" sys.path entries mentioning hermes/agent:") + for entry in relevant[:6]: + lines.append(f" - {entry}") + if len(relevant) > 6: + lines.append(f" ... and {len(relevant) - 6} more") + else: + lines.append(" sys.path: (no entries mention hermes or agent)") + + lines.append("") + lines.append(" Most common fix: install the agent in editable mode so its modules") + lines.append(" appear on sys.path:") + lines.append("") + lines.append(" cd /path/to/hermes-agent") + lines.append(" pip install -e .") + lines.append("") + lines.append(" Then restart the WebUI.") + lines.append("") + lines.append(' Full troubleshooting: docs/troubleshooting.md ("AIAgent not available")') + return "\n".join(lines) from api.models import get_session, title_from from api.workspace import set_last_workspace @@ -1958,7 +2007,7 @@ def _run_agent_streaming( _AIAgent = _get_ai_agent() if _AIAgent is None: - raise ImportError("AIAgent not available -- check that hermes-agent is on sys.path") + raise ImportError(_aiagent_import_error_detail()) # Initialize SessionDB so session_search works in WebUI sessions _session_db = None diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..0a9a4c45 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,97 @@ +# Troubleshooting + +Concrete diagnostic flows for the most common failure modes when running Hermes WebUI. Each entry has the symptom, the diagnostic commands you should run *before* opening an issue, and the fix that has worked for past reporters. + +If your symptom isn't listed and the diagnostics don't narrow it down, file a bug at https://github.com/nesquena/hermes-webui/issues — include the **full output** of every command in the relevant section. + +--- + +## "AIAgent not available -- check that hermes-agent is on sys.path" + +**Symptom.** WebUI starts, shows the chat interface, but every chat request fails immediately with this error in the response or the server log. As of v0.51.6 the error includes a diagnostic block with the running Python interpreter, the relevant `sys.path` entries, and the most-common fix; on older versions the message is bare. + +**Why it happens.** The WebUI imports the agent class at chat time via `from run_agent import AIAgent`. That import only succeeds if the running Python's `sys.path` contains either the hermes-agent checkout or a pip-installed copy of the agent. Three common failure modes: + +1. **Agent installed but not on `sys.path`.** Most common. The agent is checked out somewhere (e.g. `~/Programmes/hermes-agent`), the WebUI was launched with a Python that doesn't know about it, and there's no `pip install -e .` linking the two. +2. **Symlink with a typo or wrong target.** A symlink to the agent looks correct on `ls`, but `readlink` resolves to a path that doesn't exist or doesn't contain `agent/__init__.py`. +3. **`HERMES_WEBUI_AGENT_DIR` set to the wrong directory.** Override env var beats auto-discovery and points at a directory that has no agent code. + +### Step 1 — confirm the agent location + +```bash +# If you have ~/hermes-agent (the default location): +ls -la ~/hermes-agent +readlink ~/hermes-agent # if it's a symlink, where does it resolve? +ls ~/hermes-agent/agent/__init__.py 2>&1 +``` + +The third command must succeed (the file must exist). If it fails, your symlink is broken or pointing at a directory that's missing the agent module — fix that first. + +### Step 2 — confirm the WebUI is using the right Python + +```bash +cd ~/hermes-webui && ./start.sh 2>&1 | grep -iE 'agent|python|hermes_webui_python' | head -20 +``` + +The startup banner prints which Python and agent dir it resolved. If the agent dir is empty or the Python is the wrong one, set the override: + +```bash +export HERMES_WEBUI_AGENT_DIR=/absolute/path/to/hermes-agent +export HERMES_WEBUI_PYTHON=/absolute/path/to/agent/venv/bin/python +./start.sh +``` + +### Step 3 — install the agent in editable mode + +This is the most common fix and resolves the original issue #1695: + +```bash +cd /path/to/hermes-agent # the directory holding pyproject.toml + the agent/ module +pip install -e . # use the same python that runs the WebUI +``` + +Then restart the WebUI: + +```bash +cd ~/hermes-webui +./start.sh +``` + +### Step 4 — verify by importing manually + +If steps 1-3 still don't work, check whether the WebUI's Python can import the agent at all: + +```bash +$HERMES_WEBUI_PYTHON -c "from run_agent import AIAgent; print('ok')" 2>&1 +``` + +(Replace `$HERMES_WEBUI_PYTHON` with the actual Python path from step 2 if the env var isn't set.) If this prints `ok`, the agent IS on `sys.path` for that Python — and the WebUI should work. + +If this fails, `import run_agent` itself is broken — check that the agent's pyproject.toml lists `run_agent` as a top-level module or that the agent dir is on PYTHONPATH: + +```bash +PYTHONPATH=/path/to/hermes-agent $HERMES_WEBUI_PYTHON -c "from run_agent import AIAgent; print('ok')" +``` + +If adding PYTHONPATH fixes it, persist the path either via `pip install -e .` (preferred) or by setting `HERMES_WEBUI_AGENT_DIR` to that directory. + +### When to file a bug + +If after running steps 1-4 the import still fails *and* `pip install -e .` succeeded *and* `PYTHONPATH=... python -c "from run_agent import AIAgent"` succeeds — that's a real WebUI bug. File at https://github.com/nesquena/hermes-webui/issues with: + +- The output of every command in steps 1-4 +- The full diagnostic block printed by the WebUI's `ImportError` (v0.51.6+) +- Your OS, Python version, and how the agent was installed + +--- + +## Other troubleshooting + +This document grows over time. If a recurring failure mode isn't covered here yet, add it via PR. The format for each entry: **Symptom → Why → Diagnostic commands → Fix → When to file a bug**. + +Related references: + +- [`docs/supervisor.md`](supervisor.md) — process-supervisor setup (launchd, systemd, supervisord, runit/s6) including the bootstrap supervisor-foreground flag. +- [`docs/docker.md`](docker.md) — Docker compose setup, common failure modes, bind-mount migration. +- [`docs/wsl-autostart.md`](wsl-autostart.md) — WSL2 auto-start at login on Windows. +- [`docs/EXTENSIONS.md`](EXTENSIONS.md) — WebUI extension injection, security model, examples. diff --git a/tests/test_1695_aiagent_import_error_detail.py b/tests/test_1695_aiagent_import_error_detail.py new file mode 100644 index 00000000..d1a55ed1 --- /dev/null +++ b/tests/test_1695_aiagent_import_error_detail.py @@ -0,0 +1,184 @@ +"""Tests for #1695 — diagnostic detail in the "AIAgent not available" ImportError. + +Patrick-81 reported a symlinked hermes-agent install that produced a bare +"AIAgent not available -- check that hermes-agent is on sys.path" error with +no information about which Python was running, where it was looking, or what +to do next. The maintainer's response (which Patrick confirmed worked) +amounted to: run three diagnostic commands, then `pip install -e .` in the +agent dir. + +This test suite locks the diagnostic shape of the new error message: + + - The original message string is preserved (so existing log scrapers / + monitoring / docs-search keep working). + - The running python interpreter path is included. + - HERMES_WEBUI_AGENT_DIR is shown if set, "(not set)" otherwise. + - The relevant sys.path entries are shown. + - A pip install -e . hint is included. + - A pointer to docs/troubleshooting.md is included. + +Behavioural test for the actual raise path lives in the streaming integration +suite; this file only exercises the helper. +""" +import os +import sys +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def _import_helper(): + """Import _aiagent_import_error_detail without triggering the full streaming + module side-effects. + + api/streaming.py imports a lot at top-level (gateway routing, model resolver, + session DB, ...). For a focused unit test we just need the helper. Importing + the module is fine — it stays cached for the rest of the suite. + """ + sys.path.insert(0, str(REPO_ROOT)) + from api import streaming # noqa: F401 + return streaming._aiagent_import_error_detail + + +class TestAIAgentImportErrorDetail: + """Unit tests for the diagnostic helper.""" + + def test_preserves_original_message_for_log_scrapers(self): + """The original error string must remain the FIRST line so existing + scrapers, alerting, and docs-search keep matching. + """ + helper = _import_helper() + out = helper() + first = out.splitlines()[0] + assert first == "AIAgent not available -- check that hermes-agent is on sys.path", ( + f"first line must be the original error message verbatim, got: {first!r}" + ) + + def test_includes_running_python_interpreter(self): + """The diagnostic must include the running python so the user knows + which interpreter is missing the agent (most common cause of the bug). + """ + helper = _import_helper() + out = helper() + assert "python:" in out + assert sys.executable in out, ( + f"running python ({sys.executable}) must appear in the diagnostic" + ) + + def test_shows_agent_dir_env_when_set(self, monkeypatch): + """If HERMES_WEBUI_AGENT_DIR is set, the diagnostic must show its value + so the user can confirm whether the override is pointing at the right + directory. + """ + helper = _import_helper() + monkeypatch.setenv("HERMES_WEBUI_AGENT_DIR", "/custom/agent/path") + out = helper() + assert "HERMES_WEBUI_AGENT_DIR: /custom/agent/path" in out + + def test_shows_agent_dir_env_unset_marker(self, monkeypatch): + """If HERMES_WEBUI_AGENT_DIR is NOT set, the diagnostic must say so + explicitly — silence is ambiguous (could be empty string, could be unset). + """ + helper = _import_helper() + monkeypatch.delenv("HERMES_WEBUI_AGENT_DIR", raising=False) + out = helper() + assert "HERMES_WEBUI_AGENT_DIR: (not set)" in out + + def test_includes_pip_install_editable_hint(self): + """The most common fix (per #1695) is `pip install -e .` in the agent dir. + The diagnostic must surface this as the first-line remediation. + """ + helper = _import_helper() + out = helper() + assert "pip install -e ." in out, ( + "diagnostic must surface `pip install -e .` as the most common fix" + ) + + def test_points_at_troubleshooting_doc(self): + """The diagnostic must point at the docs/troubleshooting.md entry so + users with edge-case failures know where to look next. + """ + helper = _import_helper() + out = helper() + assert "troubleshooting" in out.lower(), ( + "diagnostic must point at docs/troubleshooting.md for further help" + ) + + def test_lists_sys_path_entries_when_relevant(self, monkeypatch): + """If sys.path contains entries mentioning hermes/agent, the diagnostic + must list them (helps the user confirm the agent dir is or isn't + actually present on the import path). + """ + helper = _import_helper() + # Force at least one relevant entry into sys.path for the test. + monkeypatch.syspath_prepend("/fake/hermes-agent") + out = helper() + assert "/fake/hermes-agent" in out + + def test_handles_no_relevant_sys_path_entries(self, monkeypatch): + """If sys.path has NO hermes/agent-related entries, the diagnostic must + say so explicitly — this is itself a strong diagnostic signal. + """ + helper = _import_helper() + # Replace sys.path with entries that mention neither hermes nor agent. + # Use monkeypatch.setattr so the change reverts cleanly. + clean_path = ["/usr/lib/python3.11", "/usr/local/lib/python3.11", "/tmp"] + monkeypatch.setattr(sys, "path", clean_path) + out = helper() + assert "no entries mention hermes or agent" in out, ( + "diagnostic must explicitly call out empty-path case (it's a strong signal)" + ) + + def test_output_is_multiline_string(self): + """The diagnostic must be a multi-line string (newline-joined), not a + single long line — log-readability matters when this surfaces in a + traceback. + """ + helper = _import_helper() + out = helper() + assert "\n" in out, "diagnostic must be multi-line for log readability" + assert len(out.splitlines()) >= 5, ( + f"diagnostic must have at least 5 lines, got {len(out.splitlines())}" + ) + + +class TestAIAgentImportErrorDocsPresence: + """Regression: the docs/troubleshooting.md file must exist with the + "AIAgent not available" entry the diagnostic links to. + """ + + def test_troubleshooting_md_exists(self): + path = REPO_ROOT / "docs" / "troubleshooting.md" + assert path.exists(), "docs/troubleshooting.md must exist (referenced by streaming.py)" + + def test_troubleshooting_md_has_aiagent_section(self): + path = REPO_ROOT / "docs" / "troubleshooting.md" + content = path.read_text(encoding="utf-8") + assert "AIAgent not available" in content, ( + "docs/troubleshooting.md must have an entry titled \"AIAgent not available\"" + ) + + def test_troubleshooting_md_includes_pip_install_editable(self): + """The doc must surface the `pip install -e .` fix.""" + path = REPO_ROOT / "docs" / "troubleshooting.md" + content = path.read_text(encoding="utf-8") + assert "pip install -e ." in content, ( + "docs/troubleshooting.md must include the pip install -e . fix" + ) + + def test_troubleshooting_md_describes_diagnostic_steps(self): + """The doc must walk through diagnostic commands (readlink, ls, etc.) + before jumping to the fix — that ordering is what worked for #1695. + """ + path = REPO_ROOT / "docs" / "troubleshooting.md" + content = path.read_text(encoding="utf-8") + # Look for the symlink-resolution diagnostic chain. + assert "readlink" in content, ( + "diagnostic flow must include `readlink` for the symlink-typo failure mode" + ) + assert "/agent/__init__.py" in content, ( + "diagnostic flow must verify the agent module file is reachable" + )