fix: validate WebUI launcher can import agent
This commit is contained in:
51
bootstrap.py
51
bootstrap.py
@@ -124,15 +124,50 @@ def discover_launcher_python(agent_dir: Path | None) -> str:
|
||||
return shutil.which("python3") or shutil.which("python") or sys.executable
|
||||
|
||||
|
||||
def ensure_python_has_webui_deps(python_exe: str) -> str:
|
||||
def _python_can_run_webui_and_agent(python_exe: str, agent_dir: Path | None = None) -> bool:
|
||||
script = "import yaml\nfrom run_agent import AIAgent\n"
|
||||
env = os.environ.copy()
|
||||
if agent_dir:
|
||||
env["PYTHONPATH"] = (
|
||||
str(agent_dir)
|
||||
if not env.get("PYTHONPATH")
|
||||
else f"{agent_dir}{os.pathsep}{env['PYTHONPATH']}"
|
||||
)
|
||||
check = subprocess.run(
|
||||
[python_exe, "-c", "import yaml"],
|
||||
[python_exe, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
if check.returncode == 0:
|
||||
return check.returncode == 0
|
||||
|
||||
|
||||
def ensure_python_has_webui_deps(python_exe: str, agent_dir: Path | None = None) -> str:
|
||||
"""Return a Python executable that can run both WebUI and Hermes Agent.
|
||||
|
||||
The WebUI can be launched directly with its local .venv. That venv has the
|
||||
WebUI dependencies (for example PyYAML), but may not have Hermes Agent on its
|
||||
import path. In that case the server starts healthy, then chat fails later
|
||||
with "AIAgent not available". Prefer the agent venv when it is usable, and
|
||||
validate the final interpreter before starting the server.
|
||||
"""
|
||||
if _python_can_run_webui_and_agent(python_exe, agent_dir):
|
||||
return python_exe
|
||||
|
||||
agent_candidates: list[Path] = []
|
||||
if agent_dir:
|
||||
for rel in (
|
||||
"venv/bin/python",
|
||||
"venv/Scripts/python.exe",
|
||||
".venv/bin/python",
|
||||
".venv/Scripts/python.exe",
|
||||
):
|
||||
agent_candidates.append(agent_dir / rel)
|
||||
for candidate in agent_candidates:
|
||||
if str(candidate) != python_exe and candidate.exists():
|
||||
if _python_can_run_webui_and_agent(str(candidate), agent_dir):
|
||||
return str(candidate)
|
||||
|
||||
venv_dir = REPO_ROOT / ".venv"
|
||||
venv_python = venv_dir / (
|
||||
"Scripts/python.exe" if platform.system() == "Windows" else "bin/python"
|
||||
@@ -158,7 +193,13 @@ def ensure_python_has_webui_deps(python_exe: str) -> str:
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
return str(venv_python)
|
||||
if _python_can_run_webui_and_agent(str(venv_python), agent_dir):
|
||||
return str(venv_python)
|
||||
raise RuntimeError(
|
||||
"Python environment cannot import both WebUI dependencies and Hermes Agent. "
|
||||
"Set HERMES_WEBUI_PYTHON to the Hermes Agent venv Python or install the "
|
||||
"WebUI requirements into that environment."
|
||||
)
|
||||
|
||||
|
||||
def hermes_command_exists() -> bool:
|
||||
@@ -298,7 +339,7 @@ def main() -> int:
|
||||
install_hermes_agent()
|
||||
agent_dir = discover_agent_dir()
|
||||
|
||||
python_exe = ensure_python_has_webui_deps(discover_launcher_python(agent_dir))
|
||||
python_exe = ensure_python_has_webui_deps(discover_launcher_python(agent_dir), agent_dir)
|
||||
state_dir = Path(
|
||||
os.getenv("HERMES_WEBUI_STATE_DIR", str(Path.home() / ".hermes" / "webui"))
|
||||
).expanduser()
|
||||
|
||||
@@ -305,6 +305,7 @@ def test_server():
|
||||
# causing onboarding writes (config.yaml, .env) to land in the production
|
||||
# ~/.hermes/profiles/webui/ and overwrite real API keys.
|
||||
"HERMES_BASE_HOME": str(TEST_STATE_DIR),
|
||||
"HERMES_WEBUI_PASSWORD": "",
|
||||
})
|
||||
|
||||
# Pass agent dir if discovered so server.py doesn't have to re-discover
|
||||
|
||||
42
tests/test_bootstrap_python_selection.py
Normal file
42
tests/test_bootstrap_python_selection.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pathlib
|
||||
|
||||
import bootstrap
|
||||
|
||||
|
||||
def test_ensure_python_prefers_agent_venv_when_launcher_cannot_import_agent(monkeypatch, tmp_path):
|
||||
"""Avoid starting WebUI with a local venv that later cannot import AIAgent."""
|
||||
local_python = tmp_path / "webui" / ".venv" / "bin" / "python"
|
||||
agent_python = tmp_path / "agent" / "venv" / "bin" / "python"
|
||||
agent_python.parent.mkdir(parents=True)
|
||||
agent_python.write_text("", encoding="utf-8")
|
||||
|
||||
probes = []
|
||||
|
||||
def fake_can_run(python_exe: str, agent_dir: pathlib.Path | None = None) -> bool:
|
||||
probes.append(pathlib.Path(python_exe))
|
||||
return pathlib.Path(python_exe) == agent_python
|
||||
|
||||
monkeypatch.setattr(bootstrap, "_python_can_run_webui_and_agent", fake_can_run)
|
||||
|
||||
selected = bootstrap.ensure_python_has_webui_deps(str(local_python), tmp_path / "agent")
|
||||
|
||||
assert selected == str(agent_python)
|
||||
assert probes == [local_python, agent_python]
|
||||
|
||||
|
||||
def test_ensure_python_fails_loudly_when_no_interpreter_can_import_agent(monkeypatch, tmp_path):
|
||||
"""Do not report health OK when chat would fail with missing AIAgent."""
|
||||
local_python = tmp_path / "webui" / ".venv" / "bin" / "python"
|
||||
agent_python = tmp_path / "agent" / "venv" / "bin" / "python"
|
||||
agent_python.parent.mkdir(parents=True)
|
||||
agent_python.write_text("", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(bootstrap, "_python_can_run_webui_and_agent", lambda *a, **k: False)
|
||||
monkeypatch.setattr(bootstrap.subprocess, "run", lambda *a, **k: None)
|
||||
|
||||
try:
|
||||
bootstrap.ensure_python_has_webui_deps(str(local_python), tmp_path / "agent")
|
||||
except RuntimeError as exc:
|
||||
assert "cannot import both WebUI dependencies and Hermes Agent" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected RuntimeError")
|
||||
Reference in New Issue
Block a user