from __future__ import annotations import argparse import json import shutil import subprocess import sys import urllib.error import urllib.request from dataclasses import asdict, dataclass from pathlib import Path from typing import Any, Literal ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) DEFAULT_BASE_URL = "http://127.0.0.1:8080/v1" DEFAULT_MODEL = "signal-garden-qwen35-code-mutation" REQUIRED_LLAMA_HELP_TERMS = ( "-hf", "--alias", "--ctx-size", "--host", "--port", "--reasoning", "--reasoning-budget", ) Status = Literal["ok", "warn", "fail"] @dataclass(frozen=True) class CheckResult: name: str status: Status detail: str def check_python_app() -> CheckResult: try: import gradio as gr import numpy as np import app app.build_demo(app.ArcadeConfig()) except Exception as exc: return CheckResult("python-app", "fail", f"Gradio app import/build failed: {exc}") return CheckResult( "python-app", "ok", f"Gradio {getattr(gr, '__version__', 'unknown')} and NumPy {np.__version__} import; app builds.", ) def check_uv() -> CheckResult: uv_path = shutil.which("uv") if uv_path is None: return CheckResult("uv", "fail", "`uv` is not on PATH; repository commands use `uv run ...`.") return CheckResult("uv", "ok", f"`uv` found at {uv_path}.") def check_llama_server() -> CheckResult: llama_path = shutil.which("llama-server") if llama_path is None: return CheckResult( "llama-server", "fail", "`llama-server` is not on PATH; install llama.cpp or use the Docker Space build.", ) try: completed = subprocess.run( [llama_path, "--help"], check=False, capture_output=True, text=True, timeout=15, ) except subprocess.TimeoutExpired: return CheckResult("llama-server", "fail", f"`{llama_path} --help` timed out.") except OSError as exc: return CheckResult("llama-server", "fail", f"Could not run `{llama_path} --help`: {exc}") help_text = f"{completed.stdout}\n{completed.stderr}" if completed.returncode != 0: return CheckResult("llama-server", "fail", f"`{llama_path} --help` exited {completed.returncode}.") missing = [term for term in REQUIRED_LLAMA_HELP_TERMS if term not in help_text] if missing: return CheckResult( "llama-server", "fail", f"`{llama_path}` is missing required llama.cpp server flag(s): {', '.join(missing)}.", ) return CheckResult( "llama-server", "ok", f"`llama-server` found at {llama_path} with HF, alias, context, host/port, and reasoning flags.", ) def check_llm_endpoint(base_url: str, model: str, timeout_seconds: float, required: bool) -> CheckResult: endpoint = f"{base_url.rstrip('/')}/models" failure_status: Status = "fail" if required else "warn" try: with urllib.request.urlopen(endpoint, timeout=max(0.1, float(timeout_seconds))) as response: body = response.read().decode("utf-8") except urllib.error.URLError as exc: return CheckResult( "llm-endpoint", failure_status, f"{endpoint} is not reachable: {exc.reason}. Start llama-server before playing LLM rounds.", ) except TimeoutError: return CheckResult( "llm-endpoint", failure_status, f"{endpoint} timed out after {timeout_seconds:g}s.", ) try: payload = json.loads(body) except json.JSONDecodeError as exc: return CheckResult("llm-endpoint", failure_status, f"{endpoint} returned malformed JSON: {exc.msg}.") ids = _model_ids(payload) if ids and model not in ids: return CheckResult( "llm-endpoint", failure_status, f"{endpoint} is live, but model alias `{model}` was not listed. Available: {', '.join(ids)}.", ) if not ids: return CheckResult("llm-endpoint", "warn", f"{endpoint} is live, but no model ids were listed.") return CheckResult("llm-endpoint", "ok", f"{endpoint} is live and lists `{model}`.") def run_checks( base_url: str = DEFAULT_BASE_URL, model: str = DEFAULT_MODEL, timeout_seconds: float = 5.0, require_live_llm: bool = False, ) -> list[CheckResult]: return [ check_python_app(), check_uv(), check_llama_server(), check_llm_endpoint(base_url, model, timeout_seconds, require_live_llm), ] def _model_ids(payload: Any) -> tuple[str, ...]: if not isinstance(payload, dict): return () data = payload.get("data") if not isinstance(data, list): return () ids: list[str] = [] for item in data: if isinstance(item, dict) and isinstance(item.get("id"), str): ids.append(item["id"]) return tuple(ids) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Check Signal Garden Gradio and LLM runtime requirements.") parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("--model", default=DEFAULT_MODEL) parser.add_argument("--timeout-seconds", type=float, default=5.0) parser.add_argument( "--require-live-llm", action="store_true", help="Fail when the OpenAI-compatible /models endpoint is not reachable or has the wrong model alias.", ) parser.add_argument("--json", action="store_true", help="Print machine-readable check results.") args = parser.parse_args(argv) results = run_checks( base_url=args.base_url, model=args.model, timeout_seconds=max(0.1, float(args.timeout_seconds)), require_live_llm=bool(args.require_live_llm), ) if args.json: print(json.dumps([asdict(result) for result in results], indent=2, sort_keys=True)) else: for result in results: print(f"{result.status.upper():4} {result.name}: {result.detail}") return 1 if any(result.status == "fail" for result in results) else 0 if __name__ == "__main__": raise SystemExit(main())