fix: validate WebUI launcher can import agent
This commit is contained in:
parent
b8a346f421
commit
634f90a807
3 changed files with 89 additions and 5 deletions
49
bootstrap.py
49
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
|
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(
|
check = subprocess.run(
|
||||||
[python_exe, "-c", "import yaml"],
|
[python_exe, "-c", script],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=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
|
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_dir = REPO_ROOT / ".venv"
|
||||||
venv_python = venv_dir / (
|
venv_python = venv_dir / (
|
||||||
"Scripts/python.exe" if platform.system() == "Windows" else "bin/python"
|
"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,
|
check=True,
|
||||||
)
|
)
|
||||||
|
if _python_can_run_webui_and_agent(str(venv_python), agent_dir):
|
||||||
return str(venv_python)
|
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:
|
def hermes_command_exists() -> bool:
|
||||||
|
|
@ -298,7 +339,7 @@ def main() -> int:
|
||||||
install_hermes_agent()
|
install_hermes_agent()
|
||||||
agent_dir = discover_agent_dir()
|
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(
|
state_dir = Path(
|
||||||
os.getenv("HERMES_WEBUI_STATE_DIR", str(Path.home() / ".hermes" / "webui"))
|
os.getenv("HERMES_WEBUI_STATE_DIR", str(Path.home() / ".hermes" / "webui"))
|
||||||
).expanduser()
|
).expanduser()
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,7 @@ def test_server():
|
||||||
# causing onboarding writes (config.yaml, .env) to land in the production
|
# causing onboarding writes (config.yaml, .env) to land in the production
|
||||||
# ~/.hermes/profiles/webui/ and overwrite real API keys.
|
# ~/.hermes/profiles/webui/ and overwrite real API keys.
|
||||||
"HERMES_BASE_HOME": str(TEST_STATE_DIR),
|
"HERMES_BASE_HOME": str(TEST_STATE_DIR),
|
||||||
|
"HERMES_WEBUI_PASSWORD": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Pass agent dir if discovered so server.py doesn't have to re-discover
|
# 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")
|
||||||
Loading…
Reference in a new issue