chore(core): 🔧 Update run configuration
This commit is contained in:
parent
6fd830a7a7
commit
fd6f24dc03
6 changed files with 471 additions and 0 deletions
1
run
Symbolic link
1
run
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
scripts/run/script_runner.py
|
||||
1
scripts/run/__init__.py
Normal file
1
scripts/run/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""@model-boss script runner package."""
|
||||
127
scripts/run/dev_command.py
Normal file
127
scripts/run/dev_command.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""Dev command handler for @model-boss script runner.
|
||||
|
||||
Starts development servers using configuration from infrastructure/ports.yaml.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from service_config import get_service_config, list_services
|
||||
|
||||
|
||||
def dev_command(args: list[str], workspace_root: Path) -> int:
|
||||
"""Start development servers.
|
||||
|
||||
Args:
|
||||
args: Command-line arguments
|
||||
workspace_root: Path to workspace root
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success, non-zero = failure)
|
||||
"""
|
||||
services = list_services("dev")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="./run dev",
|
||||
description="Start development servers",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
./run dev coordinator # Start GPU coordinator
|
||||
./run dev llama-http # Start LLM backend
|
||||
./run dev --list # List all available services
|
||||
|
||||
Available services:
|
||||
coordinator GPU/model coordination (port 8210)
|
||||
llama-http LLM backend with llama.cpp (port 10010)
|
||||
|
||||
Note: Services run in foreground. Use Ctrl+C to stop.
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"service",
|
||||
nargs="?",
|
||||
choices=services,
|
||||
help="Service to start",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available services",
|
||||
)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
|
||||
# List services
|
||||
if parsed.list:
|
||||
print("Development services:\n")
|
||||
for svc_id in services:
|
||||
cfg = get_service_config(svc_id, "dev")
|
||||
print(f" {svc_id:15} port {cfg['port']}")
|
||||
return 0
|
||||
|
||||
# Require service argument
|
||||
if not parsed.service:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
cfg = get_service_config(parsed.service, "dev")
|
||||
service_dir = workspace_root / cfg["dir"]
|
||||
|
||||
if not service_dir.exists():
|
||||
print(f"Error: Service directory not found: {service_dir}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Starting {parsed.service} on port {cfg['port']}...")
|
||||
print(f"Directory: {service_dir}")
|
||||
print()
|
||||
|
||||
# Check for venv
|
||||
venv_path = service_dir / ".venv"
|
||||
if not venv_path.exists():
|
||||
print("Warning: No .venv found. You may need to create one:")
|
||||
print(f" cd {service_dir}")
|
||||
print(" python -m venv .venv")
|
||||
print(" source .venv/bin/activate")
|
||||
print(" pip install -e .")
|
||||
print()
|
||||
|
||||
# Use python -m to run the service module
|
||||
cmd = ["python", "-m", cfg["module"]]
|
||||
|
||||
# For Python services, activate venv
|
||||
activate_script = venv_path / "bin" / "activate"
|
||||
if activate_script.exists():
|
||||
full_cmd = f"source {activate_script} && {' '.join(cmd)}"
|
||||
cmd = ["bash", "-c", full_cmd]
|
||||
|
||||
print(f"Command: python -m {cfg['module']}")
|
||||
print()
|
||||
print("Press Ctrl+C to stop")
|
||||
print("-" * 50)
|
||||
print()
|
||||
|
||||
# Run service
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=service_dir, check=False)
|
||||
return result.returncode
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nStopped by user")
|
||||
return 0
|
||||
|
||||
|
||||
def register_dev_command(runner):
|
||||
"""Register the dev command with the script runner.
|
||||
|
||||
Args:
|
||||
runner: ScriptRunner instance
|
||||
"""
|
||||
runner.register_command(
|
||||
"dev",
|
||||
dev_command,
|
||||
"Start development servers",
|
||||
)
|
||||
129
scripts/run/prod_command.py
Normal file
129
scripts/run/prod_command.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""Production server command for @model-boss.
|
||||
|
||||
Starts production servers using configuration from infrastructure/ports.production.yaml.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from service_config import get_service_config, list_services
|
||||
|
||||
|
||||
def prod_command(args: list[str], workspace_root: Path) -> int:
|
||||
"""Start production servers.
|
||||
|
||||
Args:
|
||||
args: Command-line arguments
|
||||
workspace_root: Path to workspace root
|
||||
|
||||
Returns:
|
||||
Exit code (0 = success, non-zero = failure)
|
||||
"""
|
||||
services = list_services("prod")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="./run prod",
|
||||
description="Start production servers",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
./run prod coordinator # Start coordinator on prod port
|
||||
./run prod llama-http # Start LLM backend on prod port
|
||||
./run prod --list # List prod services with settings
|
||||
|
||||
Note: Production ports are dev + 10000 (e.g., 8210 -> 18210).
|
||||
This allows running dev and prod simultaneously.
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"service",
|
||||
nargs="?",
|
||||
choices=services,
|
||||
help="Service to start",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available services with production settings",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--workers",
|
||||
type=int,
|
||||
help="Override worker count",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
help="Override log level",
|
||||
)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
|
||||
if parsed.list:
|
||||
print("Production services:\n")
|
||||
for svc_id in services:
|
||||
cfg = get_service_config(svc_id, "prod")
|
||||
timeout = cfg["timeout_keep_alive"]
|
||||
print(
|
||||
f" {svc_id:15} port {cfg['port']:5} "
|
||||
f"workers={cfg['workers']} timeout={timeout}s"
|
||||
)
|
||||
return 0
|
||||
|
||||
if not parsed.service:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
cfg = get_service_config(parsed.service, "prod")
|
||||
service_dir = workspace_root / cfg["dir"]
|
||||
|
||||
if not service_dir.exists():
|
||||
print(f"Error: {service_dir} not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Set environment variables for production port
|
||||
env = os.environ.copy()
|
||||
if parsed.service == "coordinator":
|
||||
env["MODEL_BOSS_PORT"] = str(cfg["port"])
|
||||
elif parsed.service == "llama-http":
|
||||
env["LLAMA_HTTP_PORT"] = str(cfg["port"])
|
||||
|
||||
# Use python -m to run the service module
|
||||
cmd = ["python", "-m", cfg["module"]]
|
||||
|
||||
# For Python services, activate venv
|
||||
venv = service_dir / ".venv/bin/activate"
|
||||
if venv.exists():
|
||||
# Pass environment variables through bash
|
||||
env_exports = " ".join(f"{k}={v}" for k, v in env.items() if k.startswith(("MODEL_BOSS_", "LLAMA_HTTP_")))
|
||||
full_cmd = f"source {venv} && {env_exports} {' '.join(cmd)}"
|
||||
cmd = ["bash", "-c", full_cmd]
|
||||
|
||||
print(f"Starting {parsed.service} (production) on port {cfg['port']}")
|
||||
print(f"Workers: {parsed.workers or cfg['workers']}")
|
||||
print(f"Timeout: {cfg['timeout_keep_alive']}s")
|
||||
print()
|
||||
|
||||
try:
|
||||
return subprocess.run(cmd, cwd=service_dir, env=env).returncode
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
|
||||
|
||||
def register_prod_command(runner):
|
||||
"""Register the prod command with the script runner.
|
||||
|
||||
Args:
|
||||
runner: ScriptRunner instance
|
||||
"""
|
||||
runner.register_command(
|
||||
"prod",
|
||||
prod_command,
|
||||
"Start production servers",
|
||||
)
|
||||
126
scripts/run/script_runner.py
Executable file
126
scripts/run/script_runner.py
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
@model-boss Workspace Script Runner
|
||||
|
||||
Unified command runner for the @model-boss workspace.
|
||||
|
||||
Usage:
|
||||
./run dev <service> # Start development server
|
||||
./run prod <service> # Start production server
|
||||
./run --help # Show all available commands
|
||||
./run <command> --help # Show command-specific help
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ScriptRunner:
|
||||
"""Main script runner that dispatches to command handlers."""
|
||||
|
||||
def __init__(self):
|
||||
# Get the actual script location (resolves symlinks)
|
||||
script_path = Path(__file__).resolve()
|
||||
# workspace_root is 3 levels up from script_runner.py
|
||||
# script_runner.py -> run/ -> scripts/ -> workspace/
|
||||
self.workspace_root = script_path.parent.parent.parent
|
||||
self.commands = {}
|
||||
|
||||
def register_command(self, name: str, handler, help_text: str):
|
||||
"""Register a command handler."""
|
||||
self.commands[name] = {
|
||||
"handler": handler,
|
||||
"help": help_text,
|
||||
}
|
||||
|
||||
def run(self, args=None):
|
||||
"""Parse arguments and dispatch to command handler."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="./run",
|
||||
description="@model-boss workspace script runner",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=self._build_command_list(),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=list(self.commands.keys()),
|
||||
help="Command to run",
|
||||
)
|
||||
|
||||
# Parse just the command first
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
if not args or args[0] in ["-h", "--help"]:
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
command_name = args[0]
|
||||
command_args = args[1:]
|
||||
|
||||
if command_name not in self.commands:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
# Dispatch to command handler
|
||||
command = self.commands[command_name]
|
||||
try:
|
||||
return command["handler"](command_args, self.workspace_root)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nInterrupted by user")
|
||||
return 130
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
def _build_command_list(self):
|
||||
"""Build formatted command list for help text."""
|
||||
if not self.commands:
|
||||
return ""
|
||||
|
||||
lines = ["\nAvailable commands:"]
|
||||
max_len = max(len(name) for name in self.commands.keys())
|
||||
|
||||
for name, cmd in sorted(self.commands.items()):
|
||||
padding = " " * (max_len - len(name))
|
||||
lines.append(f" {name}{padding} {cmd['help']}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def load_command(command_path: Path):
|
||||
"""Dynamically load a command module."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(command_path.stem, command_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
runner = ScriptRunner()
|
||||
|
||||
# Resolve the actual file location (handles symlinks)
|
||||
script_path = Path(__file__).resolve()
|
||||
|
||||
# Register all commands
|
||||
commands = [
|
||||
("dev_command.py", "register_dev_command"),
|
||||
("prod_command.py", "register_prod_command"),
|
||||
]
|
||||
|
||||
for cmd_file, register_func in commands:
|
||||
cmd_path = script_path.parent / cmd_file
|
||||
if cmd_path.exists():
|
||||
cmd_module = load_command(cmd_path)
|
||||
getattr(cmd_module, register_func)(runner)
|
||||
|
||||
# Run
|
||||
sys.exit(runner.run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
scripts/run/service_config.py
Normal file
87
scripts/run/service_config.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Service configuration loader for @model-boss.
|
||||
|
||||
Loads from infrastructure/ports.yaml (service-registry compatible).
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
# Service directory mapping
|
||||
SERVICE_DIRS: dict[str, str] = {
|
||||
"coordinator": "services/coordinator/service",
|
||||
"llama-http": "services/llama-http/service",
|
||||
}
|
||||
|
||||
# App entrypoints (uvicorn app paths) - using Python module paths
|
||||
SERVICE_APPS: dict[str, str] = {
|
||||
"coordinator": "model_boss_coordinator:create_app",
|
||||
"llama-http": "llama_http:create_app",
|
||||
}
|
||||
|
||||
# Service module names for -m execution
|
||||
SERVICE_MODULES: dict[str, str] = {
|
||||
"coordinator": "model_boss_coordinator",
|
||||
"llama-http": "llama_http",
|
||||
}
|
||||
|
||||
|
||||
class PortsConfig(TypedDict, total=False):
|
||||
"""Type for ports configuration."""
|
||||
|
||||
model_boss: dict[str, int]
|
||||
runtime: dict
|
||||
|
||||
|
||||
def load_ports(config_file: str = "ports.yaml") -> PortsConfig:
|
||||
"""Load ports from infrastructure config."""
|
||||
config_path = Path(__file__).parent.parent.parent / "infrastructure" / config_file
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"Config not found: {config_path}")
|
||||
|
||||
with open(config_path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def get_service_config(service_id: str, environment: str = "dev") -> dict:
|
||||
"""Get full service configuration."""
|
||||
config_file = "ports.production.yaml" if environment == "prod" else "ports.yaml"
|
||||
ports_config = load_ports(config_file)
|
||||
|
||||
# Handle both "model-boss" and "model_boss" keys in YAML
|
||||
port_section = ports_config.get("model-boss") or ports_config.get("model_boss", {})
|
||||
port = port_section.get(service_id)
|
||||
if port is None:
|
||||
raise ValueError(f"Unknown service: {service_id}")
|
||||
|
||||
runtime = ports_config.get("runtime", {})
|
||||
|
||||
return {
|
||||
"id": service_id,
|
||||
"port": port,
|
||||
"dir": SERVICE_DIRS.get(service_id, f"services/{service_id}/service"),
|
||||
"app": SERVICE_APPS.get(service_id),
|
||||
"module": SERVICE_MODULES.get(service_id),
|
||||
"type": "python",
|
||||
"reload": runtime.get("reload", environment == "dev"),
|
||||
"log_level": runtime.get("log_level", "info"),
|
||||
"workers": runtime.get("workers", {}).get(service_id, 1),
|
||||
"timeout_keep_alive": runtime.get("timeout_keep_alive", {}).get(
|
||||
service_id, runtime.get("timeout_keep_alive", {}).get("default", 30)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def list_services(environment: str = "dev") -> list[str]:
|
||||
"""List available service IDs."""
|
||||
config_file = "ports.production.yaml" if environment == "prod" else "ports.yaml"
|
||||
ports_config = load_ports(config_file)
|
||||
# Handle both key formats
|
||||
port_section = ports_config.get("model-boss") or ports_config.get("model_boss", {})
|
||||
# Filter to only services that have directories defined
|
||||
return [
|
||||
svc_id
|
||||
for svc_id in port_section.keys()
|
||||
if svc_id in SERVICE_DIRS
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue