meridian-api / scripts /mvp_setup.py
Demon1212122's picture
feat: backend-only API β€” models downloaded at boot from astroknotsheep/meridian-models
4dea501
Raw
History Blame Contribute Delete
14.1 kB
#!/usr/bin/env python3
"""
scripts/mvp_setup.py β€” Meridian MVP developer CLI
===================================================
Usage (from Prod/ root):
python3 scripts/mvp_setup.py runserver # start FastAPI dev server
python3 scripts/mvp_setup.py check # preflight: env, venv, models
python3 scripts/mvp_setup.py health # ping /health endpoint
python3 scripts/mvp_setup.py test # run test_evaluation.py
python3 scripts/mvp_setup.py info # show config summary
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
# ── Paths ──────────────────────────────────────────────────────────────────────
ROOT = Path(__file__).resolve().parent.parent # Prod/
BACKEND = ROOT / "backend"
MODELS_DIR = BACKEND / "models"
FRONTEND = ROOT / "frontend"
ENV_FILE = ROOT / ".env"
TESTS_DIR = ROOT / "mental-health-ai" / "tests"
# Model artifacts required for diagnosis mode
REQUIRED_MODELS = {
"MentalRoBERTa ONNX": MODELS_DIR / "mentalroberta_onnx" / "model.onnx",
"SVM classifier": MODELS_DIR / "svm_depression.joblib",
"Feature scaler": MODELS_DIR / "scaler.joblib",
}
# Venv search order β€” prefer .venv, fall back to venv
VENV_CANDIDATES = [ROOT / ".venv", ROOT / "venv"]
# ── ANSI colours ───────────────────────────────────────────────────────────────
RESET = "\033[0m"
BOLD = "\033[1m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
DIM = "\033[2m"
def ok(msg: str): print(f" {GREEN}βœ“{RESET} {msg}")
def warn(msg: str): print(f" {YELLOW}⚠{RESET} {msg}")
def fail(msg: str): print(f" {RED}βœ—{RESET} {msg}")
def info(msg: str): print(f" {CYAN}β†’{RESET} {msg}")
def hdr(msg: str): print(f"\n{BOLD}{msg}{RESET}")
def dim(msg: str): print(f" {DIM}{msg}{RESET}")
# ── Helpers ────────────────────────────────────────────────────────────────────
def find_venv() -> Path | None:
"""Return the first venv directory that has a python3 binary."""
for candidate in VENV_CANDIDATES:
py = candidate / "bin" / "python3"
if py.exists():
return candidate
return None
def venv_python() -> Path:
"""Return the venv python3, or sys.executable as fallback."""
venv = find_venv()
if venv:
return venv / "bin" / "python3"
return Path(sys.executable)
def venv_bin(name: str) -> Path:
"""Return a binary from the venv/bin dir."""
venv = find_venv()
if venv:
p = venv / "bin" / name
if p.exists():
return p
return Path(name) # fallback: rely on PATH
def load_dotenv_simple() -> dict:
"""Minimal .env parser β€” no third-party deps needed for this script."""
result = {}
if not ENV_FILE.exists():
return result
for line in ENV_FILE.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
result[key.strip()] = val.strip()
return result
# ── Commands ───────────────────────────────────────────────────────────────────
def cmd_check(args) -> int:
"""
Preflight check: venv, Python version, .env, API key, model artifacts.
Returns exit code (0 = all clear, 1 = blocking issues found).
"""
hdr("Meridian β€” Preflight Check")
errors = 0
env = load_dotenv_simple()
# ── 1. Venv ─────────────────────────────────────────────────────
hdr("Virtual environment")
venv = find_venv()
if venv:
ok(f"Found venv at: {venv.relative_to(ROOT)}/")
else:
fail("No venv found. Create one:")
dim(" python3.11 -m venv venv && source venv/bin/activate")
dim(" pip install -r requirements.txt")
errors += 1
# ── 2. Python version ────────────────────────────────────────────
hdr("Python version")
py = venv_python()
result = subprocess.run([str(py), "--version"], capture_output=True, text=True)
ver = (result.stdout + result.stderr).strip()
major, minor = sys.version_info.major, sys.version_info.minor
if major == 3 and minor >= 11:
ok(f"{ver} (β‰₯ 3.11 βœ“)")
else:
warn(f"{ver} β€” recommend Python 3.11+ for ARM64 wheel compatibility")
# ── 3. .env file ─────────────────────────────────────────────────
hdr(".env file")
if ENV_FILE.exists():
ok(f".env found ({ENV_FILE.stat().st_size} bytes)")
else:
fail(".env not found. Copy the example and fill in your key:")
dim(" cp .env.example .env")
errors += 1
# ── 4. OpenAI API key ────────────────────────────────────────────
hdr("OpenAI API key")
api_key = env.get("OPENAI_API_KEY", "") or os.environ.get("OPENAI_API_KEY", "")
if not api_key:
fail("OPENAI_API_KEY not set in .env β€” therapy mode will not work")
errors += 1
elif api_key.startswith("sk-") and len(api_key) > 20:
masked = api_key[:8] + "..." + api_key[-4:]
ok(f"Key present: {masked}")
else:
warn(f"Key looks unusual (doesn't start with 'sk-'): {api_key[:12]}...")
therapy_model = env.get("THERAPY_MODEL", "gpt-4o-mini")
info(f"THERAPY_MODEL = {therapy_model}")
# ── 5. Model artifacts ───────────────────────────────────────────
hdr("ML model artifacts")
models_ok = True
for label, path in REQUIRED_MODELS.items():
if path.exists():
size_mb = path.stat().st_size / 1_000_000
ok(f"{label}: {path.relative_to(ROOT)} ({size_mb:.1f} MB)")
else:
warn(f"{label}: MISSING β€” {path.relative_to(ROOT)}")
dim(" Diagnosis mode will degrade gracefully; therapy mode still works.")
dim(" Run the Colab notebook (notebooks/train_classifiers.ipynb) to generate.")
models_ok = False
if models_ok:
ok("All 3 model artifacts present β€” diagnosis mode ready")
# ── 6. Frontend ──────────────────────────────────────────────────
hdr("Frontend")
index = FRONTEND / "index.html"
if index.exists():
ok(f"index.html found ({index.stat().st_size:,} bytes)")
else:
fail(f"frontend/index.html missing")
errors += 1
# ── Summary ──────────────────────────────────────────────────────
hdr("Summary")
if errors == 0:
print(f"\n {GREEN}{BOLD}All checks passed.{RESET} Run the server with:\n")
print(f" python3 scripts/mvp_setup.py runserver\n")
else:
print(f"\n {RED}{BOLD}{errors} blocking issue(s) found.{RESET} Fix the items above, then rerun:\n")
print(f" python3 scripts/mvp_setup.py check\n")
return 0 if errors == 0 else 1
def cmd_runserver(args) -> int:
"""
Start the FastAPI development server using the venv's uvicorn.
Equivalent to: source venv/bin/activate && uvicorn backend.main:app --reload ...
"""
hdr("Meridian β€” Starting Dev Server")
# Quick preflight: warn if key is missing but don't block
env = load_dotenv_simple()
api_key = env.get("OPENAI_API_KEY", "") or os.environ.get("OPENAI_API_KEY", "")
if not api_key:
warn("OPENAI_API_KEY not set β€” therapy mode will fail at runtime")
else:
ok(f"OPENAI_API_KEY present | model={env.get('THERAPY_MODEL', 'gpt-4o-mini')}")
for label, path in REQUIRED_MODELS.items():
if path.exists():
ok(f"{label} found")
else:
warn(f"{label} missing β€” diagnosis mode will show 'not available' message")
uvicorn = venv_bin("uvicorn")
host = getattr(args, "host", "0.0.0.0")
port = str(getattr(args, "port", 8000))
reload = not getattr(args, "no_reload", False)
cmd = [str(uvicorn), "backend.main:app", "--host", host, "--port", port]
if reload:
cmd.append("--reload")
print(f"\n {CYAN}β†’{RESET} {' '.join(cmd)}\n")
print(f" {DIM}Frontend: http://{host}:{port}{RESET}")
print(f" {DIM}Health: http://{host}:{port}/health{RESET}")
print(f" {DIM}Ctrl+C to stop{RESET}\n")
os.chdir(ROOT) # uvicorn must run from project root for module imports
try:
subprocess.run(cmd, check=True)
except KeyboardInterrupt:
print("\n Server stopped.")
except subprocess.CalledProcessError as e:
fail(f"Server exited with code {e.returncode}")
return e.returncode
return 0
def cmd_health(args) -> int:
"""Ping the /health endpoint and print the response."""
import urllib.request, json as _json
hdr("Meridian β€” Health Check")
port = getattr(args, "port", 8000)
url = f"http://localhost:{port}/health"
info(f"GET {url}")
try:
with urllib.request.urlopen(url, timeout=5) as resp:
body = _json.loads(resp.read())
ok(f"Status {resp.status} β†’ {body}")
return 0
except Exception as e:
fail(f"Could not reach server: {e}")
dim(" Is the server running? Start it with:")
dim(" python3 scripts/mvp_setup.py runserver")
return 1
def cmd_test(args) -> int:
"""Run the MentalRoBERTa evaluation test suite."""
hdr("Meridian β€” Running Test Suite")
test_file = TESTS_DIR / "test_evaluation.py"
if not test_file.exists():
fail(f"Test file not found: {test_file.relative_to(ROOT)}")
return 1
py = venv_python()
threshold = getattr(args, "threshold", None)
cmd = [str(py), str(test_file)]
if threshold is not None:
cmd += ["--threshold", str(threshold)]
info(f"Running: {' '.join(cmd)}")
print()
os.chdir(ROOT)
result = subprocess.run(cmd)
return result.returncode
def cmd_info(args) -> int:
"""Print current configuration and model status."""
hdr("Meridian β€” Configuration")
env = load_dotenv_simple()
print(f"\n {'Key':<22} {'Value'}")
print(f" {'─'*22} {'─'*40}")
for key in ("THERAPY_MODEL", "APP_ENV", "OPENAI_API_KEY"):
val = env.get(key, os.environ.get(key, "β€”"))
if key == "OPENAI_API_KEY" and val and val != "β€”":
val = val[:8] + "..." + val[-4:]
print(f" {key:<22} {val}")
hdr("Models")
for label, path in REQUIRED_MODELS.items():
status = f"{GREEN}βœ“ {path.stat().st_size/1e6:.1f} MB{RESET}" if path.exists() else f"{YELLOW}missing{RESET}"
print(f" {label:<28} {status}")
hdr("Paths")
venv = find_venv()
info(f"Project root : {ROOT}")
info(f"Active venv : {venv or 'not found'}")
info(f"Python : {venv_python()}")
print()
return 0
# ── Entrypoint ─────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
prog="mvp_setup.py",
description="Meridian MVP developer CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
commands:
runserver Start the FastAPI dev server (uses venv uvicorn)
check Preflight: venv, .env, API key, model artifacts
health Ping /health endpoint to verify server is running
test Run MentalRoBERTa evaluation test suite
info Show current config and model status
""",
)
sub = parser.add_subparsers(dest="command")
# runserver
p_run = sub.add_parser("runserver", help="Start FastAPI dev server")
p_run.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
p_run.add_argument("--port", default=8000, type=int, help="Bind port (default: 8000)")
p_run.add_argument("--no-reload", dest="no_reload", action="store_true",
help="Disable auto-reload (use in production)")
# check
sub.add_parser("check", help="Preflight checks")
# health
p_health = sub.add_parser("health", help="Ping /health endpoint")
p_health.add_argument("--port", default=8000, type=int)
# test
p_test = sub.add_parser("test", help="Run evaluation test suite")
p_test.add_argument("--threshold", type=float, default=None,
help="Classification threshold (default: 0.5)")
# info
sub.add_parser("info", help="Show config and model status")
args = parser.parse_args()
dispatch = {
"runserver": cmd_runserver,
"check": cmd_check,
"health": cmd_health,
"test": cmd_test,
"info": cmd_info,
}
if args.command is None:
parser.print_help()
return 0
fn = dispatch.get(args.command)
if fn is None:
parser.print_help()
return 1
sys.exit(fn(args))
if __name__ == "__main__":
main()