mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-14 00:47:52 +00:00
Improve onboarding CLI resilience and diagnostics (#45)
* Improve onboarding CLI resilience and diagnostics * chore: update version to 1.0.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openjarvis-chat",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -176,9 +176,9 @@ def _check_default_model() -> CheckResult:
|
||||
if not default_model:
|
||||
return CheckResult(
|
||||
"Default model",
|
||||
"warn",
|
||||
"Not configured",
|
||||
details="Set intelligence.default_model in config.toml.",
|
||||
"ok",
|
||||
"Not configured (auto-routing enabled)",
|
||||
details="Router will select a model dynamically.",
|
||||
)
|
||||
|
||||
_ensure_engines_imported()
|
||||
@@ -213,7 +213,7 @@ def _check_optional_deps() -> List[CheckResult]:
|
||||
optional_packages = [
|
||||
("fastapi", "openjarvis[server]", "REST API server"),
|
||||
("torch", "pip install torch", "SFT/GRPO training"),
|
||||
("pynvml", "openjarvis[energy-nvidia]", "NVIDIA energy monitoring"),
|
||||
("pynvml", "openjarvis[gpu-metrics]", "NVIDIA energy monitoring"),
|
||||
("amdsmi", "openjarvis[energy-amd]", "AMD energy monitoring"),
|
||||
("colbert", "openjarvis[memory-colbert]", "ColBERT memory backend"),
|
||||
("zeus", "openjarvis[energy-apple]", "Apple Silicon energy monitoring"),
|
||||
@@ -236,14 +236,17 @@ def _check_optional_deps() -> List[CheckResult]:
|
||||
|
||||
|
||||
def _check_nodejs() -> CheckResult:
|
||||
"""Check Node.js version (>= 22 required for OpenClaw)."""
|
||||
"""Check Node.js version for Node-backed integrations."""
|
||||
node_path = shutil.which("node")
|
||||
if not node_path:
|
||||
return CheckResult(
|
||||
"Node.js",
|
||||
"warn",
|
||||
"Not found",
|
||||
details="Node.js 22+ is required for OpenClaw agent.",
|
||||
details=(
|
||||
"Node.js 22+ is required for ClaudeCodeAgent and the "
|
||||
"WhatsApp Baileys channel bridge."
|
||||
),
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -262,7 +265,10 @@ def _check_nodejs() -> CheckResult:
|
||||
"Node.js",
|
||||
"warn",
|
||||
f"{version_str} (requires >= v22)",
|
||||
details="Upgrade Node.js for OpenClaw agent support.",
|
||||
details=(
|
||||
"Upgrade Node.js for ClaudeCodeAgent and WhatsApp "
|
||||
"Baileys support."
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
return CheckResult("Node.js", "warn", f"Error checking version: {exc}")
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
from rich.panel import Panel
|
||||
|
||||
from openjarvis.core.config import (
|
||||
@@ -161,7 +162,11 @@ def init(force: bool, config: Optional[Path], full_config: bool = False) -> None
|
||||
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(toml_content, title=str(DEFAULT_CONFIG_PATH), border_style="green")
|
||||
Panel(
|
||||
escape(toml_content),
|
||||
title=str(DEFAULT_CONFIG_PATH),
|
||||
border_style="green",
|
||||
)
|
||||
)
|
||||
console.print("[green]Config written successfully.[/green]")
|
||||
|
||||
@@ -177,4 +182,3 @@ def init(force: bool, config: Optional[Path], full_config: bool = False) -> None
|
||||
border_style="cyan",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,20 @@ def _check_engine_health(engine_key: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _discover_healthy_engines() -> list[str]:
|
||||
"""Return keys for all healthy engines discovered at runtime."""
|
||||
try:
|
||||
import openjarvis.engine # noqa: F401 — trigger registration
|
||||
from openjarvis.core.config import load_config
|
||||
from openjarvis.engine import _discovery
|
||||
|
||||
config = load_config()
|
||||
return [key for key, _ in _discovery.discover_engines(config)]
|
||||
except Exception as exc:
|
||||
logger.warning("Healthy engine discovery failed: %s", exc)
|
||||
return []
|
||||
|
||||
|
||||
def _check_model_available(engine_key: str) -> bool:
|
||||
"""Return True if at least one model is available on the engine."""
|
||||
try:
|
||||
@@ -104,18 +118,30 @@ def quickstart(force: bool) -> None:
|
||||
# Step 3: Check engine
|
||||
console.print()
|
||||
console.print(f"[bold cyan][3/5][/bold cyan] Checking engine ({engine_key})...")
|
||||
active_engine = engine_key
|
||||
if not _check_engine_health(engine_key):
|
||||
console.print(f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]")
|
||||
console.print()
|
||||
console.print(f" Start the {engine_key} server and try again.")
|
||||
console.print(" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
|
||||
raise SystemExit(1)
|
||||
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
|
||||
fallbacks = [k for k in _discover_healthy_engines() if k != engine_key]
|
||||
if fallbacks:
|
||||
active_engine = fallbacks[0]
|
||||
console.print(
|
||||
f" [yellow]Engine '{engine_key}' is not reachable; "
|
||||
f"falling back to '{active_engine}'.[/yellow]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]"
|
||||
)
|
||||
console.print()
|
||||
console.print(f" Start the {engine_key} server and try again.")
|
||||
console.print(" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
|
||||
|
||||
# Step 4: Verify model
|
||||
console.print()
|
||||
console.print("[bold cyan][4/5][/bold cyan] Checking for available models...")
|
||||
if not _check_model_available(engine_key):
|
||||
if not _check_model_available(active_engine):
|
||||
console.print(" [yellow]No models found.[/yellow]")
|
||||
console.print(
|
||||
" Pull a model first (e.g. [bold]ollama pull qwen3.5:3b[/bold])."
|
||||
@@ -126,7 +152,7 @@ def quickstart(force: bool) -> None:
|
||||
# Step 5: Test query
|
||||
console.print()
|
||||
console.print("[bold cyan][5/5][/bold cyan] Running test query...")
|
||||
response = _test_query(engine_key)
|
||||
response = _test_query(active_engine)
|
||||
console.print(f" [green]Response:[/green] {response[:200]}")
|
||||
|
||||
console.print()
|
||||
|
||||
@@ -11,6 +11,7 @@ from click.testing import CliRunner
|
||||
from openjarvis.cli import cli
|
||||
from openjarvis.cli.doctor_cmd import (
|
||||
_check_config_exists,
|
||||
_check_default_model,
|
||||
_check_nodejs,
|
||||
_check_python_version,
|
||||
)
|
||||
@@ -146,6 +147,17 @@ class TestCheckEngineProbing:
|
||||
assert vllm_result.status == "warn"
|
||||
|
||||
|
||||
class TestCheckDefaultModel:
|
||||
def test_empty_default_model_is_not_a_warning(self) -> None:
|
||||
"""Leaving default model empty should be treated as valid auto-routing."""
|
||||
mock_config = MagicMock()
|
||||
mock_config.intelligence.default_model = ""
|
||||
with patch("openjarvis.cli.doctor_cmd.load_config", return_value=mock_config):
|
||||
result = _check_default_model()
|
||||
assert result.status == "ok"
|
||||
assert "auto" in result.message.lower()
|
||||
|
||||
|
||||
class TestCheckNodejs:
|
||||
def test_check_nodejs_found(self) -> None:
|
||||
"""Node.js check reports version when node is available."""
|
||||
@@ -166,3 +178,5 @@ class TestCheckNodejs:
|
||||
result = _check_nodejs()
|
||||
assert result.status == "warn"
|
||||
assert "Not found" in result.message
|
||||
assert result.details is not None
|
||||
assert "OpenClaw" not in result.details
|
||||
|
||||
@@ -50,3 +50,17 @@ class TestDoctorOptionalLabels:
|
||||
]
|
||||
assert len(apple_checks) == 1
|
||||
assert "Not installed (openjarvis[energy-apple])" == apple_checks[0]["message"]
|
||||
|
||||
def test_nvidia_label_uses_current_extra_name(self) -> None:
|
||||
"""NVIDIA hint should reference the current project extra."""
|
||||
blocker = _selective_import_blocker("pynvml")
|
||||
with mock.patch("builtins.__import__", side_effect=blocker):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["doctor", "--json"])
|
||||
data = json.loads(result.output)
|
||||
nvidia_checks = [
|
||||
c for c in data
|
||||
if c["name"] == "Optional: NVIDIA energy monitoring"
|
||||
]
|
||||
assert len(nvidia_checks) == 1
|
||||
assert "Not installed (openjarvis[gpu-metrics])" == nvidia_checks[0]["message"]
|
||||
|
||||
@@ -30,6 +30,23 @@ class TestInitShowsNextSteps:
|
||||
assert "jarvis ask" in result.output
|
||||
assert "jarvis doctor" in result.output
|
||||
|
||||
def test_init_output_shows_toml_sections_literally(self, tmp_path: Path) -> None:
|
||||
"""Init output should render TOML section headers like [engine] literally."""
|
||||
config_dir = tmp_path / ".openjarvis"
|
||||
config_path = config_dir / "config.toml"
|
||||
with (
|
||||
mock.patch(
|
||||
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir
|
||||
),
|
||||
mock.patch(
|
||||
"openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path
|
||||
),
|
||||
):
|
||||
result = CliRunner().invoke(cli, ["init"])
|
||||
assert result.exit_code == 0
|
||||
assert "[engine]" in result.output
|
||||
assert "[intelligence]" in result.output
|
||||
|
||||
|
||||
class TestNextStepsOllama:
|
||||
def test_next_steps_ollama(self) -> None:
|
||||
|
||||
@@ -191,3 +191,55 @@ class TestQuickstartCommand:
|
||||
"engine" in result.output.lower()
|
||||
or "not reachable" in result.output.lower()
|
||||
)
|
||||
|
||||
def test_falls_back_to_any_healthy_engine(self, tmp_path):
|
||||
"""If the recommended engine is down, use a healthy fallback engine."""
|
||||
config_path = tmp_path / "config.toml"
|
||||
hw = MagicMock()
|
||||
hw.platform = "darwin"
|
||||
hw.cpu_brand = "Apple M1"
|
||||
hw.cpu_count = 8
|
||||
hw.ram_gb = 16
|
||||
hw.gpu = MagicMock(name="Apple GPU", vram_gb=16, count=1, vendor="apple")
|
||||
|
||||
with (
|
||||
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
|
||||
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
|
||||
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
".generate_default_toml",
|
||||
return_value="[engine]\ndefault = \"mlx\"\n",
|
||||
),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
".recommend_engine",
|
||||
return_value="mlx",
|
||||
),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
"._check_engine_health",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
"._discover_healthy_engines",
|
||||
return_value=["ollama"],
|
||||
),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
"._check_model_available",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"openjarvis.cli.quickstart_cmd"
|
||||
"._test_query",
|
||||
return_value="Hello!",
|
||||
),
|
||||
):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["quickstart"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "falling back" in result.output.lower()
|
||||
assert "ollama" in result.output
|
||||
|
||||
Reference in New Issue
Block a user