reactor-runtime 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reactor_cli/commands/__init__.py +14 -0
- reactor_cli/commands/capabilities.py +118 -0
- reactor_cli/commands/download.py +217 -0
- reactor_cli/commands/init.py +86 -0
- reactor_cli/commands/run.py +118 -0
- reactor_cli/commands/setup.py +103 -0
- reactor_cli/commands/upload.py +104 -0
- reactor_cli/main.py +113 -0
- reactor_cli/utils.py +374 -0
- reactor_runtime/__init__.py +4 -0
- reactor_runtime/context/abstract_runtime.py +216 -0
- reactor_runtime/context/context.py +59 -0
- reactor_runtime/context/local/local_coordinator.py +247 -0
- reactor_runtime/context/local/local_runtime.py +208 -0
- reactor_runtime/context/local/utils.py +30 -0
- reactor_runtime/input/input_video.py +96 -0
- reactor_runtime/livekit/livekit.py +123 -0
- reactor_runtime/model_api.py +177 -0
- reactor_runtime/output/frame_buffer.py +116 -0
- reactor_runtime/output/streamer.py +32 -0
- reactor_runtime/output/video_streamer.py +172 -0
- reactor_runtime/utils/launch.py +102 -0
- reactor_runtime/utils/loader.py +87 -0
- reactor_runtime/utils/messages.py +49 -0
- reactor_runtime/utils/schema.py +24 -0
- reactor_runtime-0.0.0.dist-info/METADATA +131 -0
- reactor_runtime-0.0.0.dist-info/RECORD +31 -0
- reactor_runtime-0.0.0.dist-info/WHEEL +5 -0
- reactor_runtime-0.0.0.dist-info/entry_points.txt +2 -0
- reactor_runtime-0.0.0.dist-info/top_level.txt +3 -0
- template/model_template.py +244 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Reactor CLI Commands
|
|
2
|
+
|
|
3
|
+
This module contains all command implementations for the reactor CLI.
|
|
4
|
+
Each command is implemented as a class following the HuggingFace pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .run import RunCommand
|
|
8
|
+
from .init import InitCommand
|
|
9
|
+
from .download import DownloadCommand
|
|
10
|
+
from .upload import UploadCommand
|
|
11
|
+
from .setup import SetupCommand
|
|
12
|
+
from .capabilities import CapabilitiesCommand
|
|
13
|
+
|
|
14
|
+
__all__ = ['RunCommand', 'InitCommand', 'DownloadCommand', 'UploadCommand', 'SetupCommand', 'CapabilitiesCommand']
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Run command implementation."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
from reactor_runtime.model_api import VideoModel, command
|
|
6
|
+
import ast
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
def load_class_without_init(file_path: str, class_name: str):
|
|
10
|
+
"""
|
|
11
|
+
Load only the subclass definition from a Python file (without executing imports or __init__),
|
|
12
|
+
recreate it with CommandBase as its parent, and call its inherited commands() method.
|
|
13
|
+
All imports from the source file are detected via AST and replicated in the namespace.
|
|
14
|
+
"""
|
|
15
|
+
source = Path(file_path).read_text(encoding="utf-8")
|
|
16
|
+
tree = ast.parse(source, filename=file_path)
|
|
17
|
+
|
|
18
|
+
def extract_imports(tree):
|
|
19
|
+
"""Return a dict of {imported_name: module_object or attribute} from the AST."""
|
|
20
|
+
imports = {}
|
|
21
|
+
|
|
22
|
+
for node in tree.body:
|
|
23
|
+
if isinstance(node, ast.Import):
|
|
24
|
+
for alias in node.names:
|
|
25
|
+
mod_name = alias.name
|
|
26
|
+
as_name = alias.asname or mod_name.split(".")[0]
|
|
27
|
+
try:
|
|
28
|
+
imports[as_name] = importlib.import_module(mod_name)
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass # Skip missing modules safely
|
|
31
|
+
|
|
32
|
+
elif isinstance(node, ast.ImportFrom):
|
|
33
|
+
if node.module is None:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
mod = importlib.import_module(node.module)
|
|
37
|
+
except ImportError:
|
|
38
|
+
continue
|
|
39
|
+
for alias in node.names:
|
|
40
|
+
as_name = alias.asname or alias.name
|
|
41
|
+
if alias.name == "*":
|
|
42
|
+
# Handle "from X import *" by copying all public symbols
|
|
43
|
+
for name in dir(mod):
|
|
44
|
+
if not name.startswith("_"):
|
|
45
|
+
imports[name] = getattr(mod, name)
|
|
46
|
+
else:
|
|
47
|
+
try:
|
|
48
|
+
imports[as_name] = getattr(mod, alias.name)
|
|
49
|
+
except AttributeError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
return imports
|
|
53
|
+
|
|
54
|
+
# Detect and import all modules referenced in the source
|
|
55
|
+
imported_symbols = extract_imports(tree)
|
|
56
|
+
|
|
57
|
+
for node in tree.body:
|
|
58
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
59
|
+
# Replace base classes with CommandBase
|
|
60
|
+
node.bases = [ast.Name(id='CommandBase', ctx=ast.Load())]
|
|
61
|
+
ast.fix_missing_locations(node)
|
|
62
|
+
|
|
63
|
+
# Compile a module containing only that class
|
|
64
|
+
class_module = ast.Module(body=[node], type_ignores=[])
|
|
65
|
+
ast.fix_missing_locations(class_module)
|
|
66
|
+
code = compile(class_module, filename=file_path, mode="exec")
|
|
67
|
+
|
|
68
|
+
# Build isolated namespace
|
|
69
|
+
ns = {
|
|
70
|
+
"CommandBase": VideoModel,
|
|
71
|
+
"command": command,
|
|
72
|
+
**imported_symbols
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exec(code, ns)
|
|
76
|
+
subcls = ns[class_name]
|
|
77
|
+
obj = subcls.__new__(subcls)
|
|
78
|
+
return obj
|
|
79
|
+
|
|
80
|
+
raise ValueError(f"Class '{class_name}' not found in {file_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CapabilitiesCommand:
|
|
84
|
+
@staticmethod
|
|
85
|
+
def register_subcommand(subparsers):
|
|
86
|
+
"""Register capabilities command"""
|
|
87
|
+
run_parser = subparsers.add_parser("capabilities", help="Print the capabilities of a reactor VideoModel.")
|
|
88
|
+
run_parser.set_defaults(func=CapabilitiesCommand)
|
|
89
|
+
|
|
90
|
+
def __init__(self, args):
|
|
91
|
+
"""Initialize with parsed arguments"""
|
|
92
|
+
self.args = args
|
|
93
|
+
|
|
94
|
+
def run(self):
|
|
95
|
+
"""Print the capabilities of a reactor VideoModel."""
|
|
96
|
+
from ..main import verify_reactor_workspace
|
|
97
|
+
|
|
98
|
+
# Verify workspace and get manifest data
|
|
99
|
+
manifest_data = verify_reactor_workspace()
|
|
100
|
+
if manifest_data is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Extract model information from manifest
|
|
104
|
+
model_class_name: str = manifest_data["class"]
|
|
105
|
+
model_file, model_class = model_class_name.split(":")
|
|
106
|
+
model_class: VideoModel = load_class_without_init(model_file+".py", model_class)
|
|
107
|
+
|
|
108
|
+
if "model_name" not in manifest_data.keys():
|
|
109
|
+
print("Error: manifest.json is missing required 'model_name' field.")
|
|
110
|
+
print("Please add a 'model_name' field specifying the model name.")
|
|
111
|
+
return
|
|
112
|
+
if "model_version" not in manifest_data.keys():
|
|
113
|
+
print("Error: manifest.json is missing required 'model_version' field.")
|
|
114
|
+
print("Please add a 'model_version' field specifying the model version.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
print(json.dumps(model_class.commands(), indent=4))
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Download command implementation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from supabase import create_client
|
|
9
|
+
from ..utils import get_weights_parallel, get_latest_version
|
|
10
|
+
from ..main import verify_reactor_workspace
|
|
11
|
+
|
|
12
|
+
# Set up logger
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_manifest(manifest_str: str) -> dict:
|
|
17
|
+
"""Parse a manifest string into a dictionary.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
manifest_str: JSON string containing the manifest
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Parsed manifest dictionary
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
return json.loads(manifest_str)
|
|
27
|
+
except json.JSONDecodeError as e:
|
|
28
|
+
logger.error(f"Failed to parse manifest JSON: {e}")
|
|
29
|
+
raise ValueError(f"Invalid manifest JSON: {e}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_weights_from_manifest(manifest: dict) -> List[str]:
|
|
33
|
+
"""Extract the weights list from a manifest dictionary.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
manifest: Dictionary containing manifest data
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of weight folder names
|
|
40
|
+
"""
|
|
41
|
+
if "weights" not in manifest:
|
|
42
|
+
logger.warning("Manifest does not contain a 'weights' field")
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
weights = manifest["weights"]
|
|
46
|
+
if not isinstance(weights, list):
|
|
47
|
+
logger.error(f"'weights' field must be a list, got {type(weights)}")
|
|
48
|
+
raise ValueError("'weights' field in manifest must be a list")
|
|
49
|
+
|
|
50
|
+
return weights
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_weights_from_local_manifest() -> List[str]:
|
|
54
|
+
"""Read weights from local manifest.json file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of weight folder names from local manifest
|
|
58
|
+
"""
|
|
59
|
+
manifest = verify_reactor_workspace()
|
|
60
|
+
if not manifest:
|
|
61
|
+
raise RuntimeError("Failed to verify reactor workspace or read manifest.json")
|
|
62
|
+
|
|
63
|
+
return get_weights_from_manifest(manifest)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_weights_from_model_id(model_id: str) -> List[str]:
|
|
67
|
+
"""Fetch model manifest from Supabase and extract weights.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
model_id: Model identifier with optional version (e.g., "matrix-2" or "matrix-2@v1.0")
|
|
71
|
+
If no version is specified, defaults to "latest"
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of weight folder names from the model's manifest
|
|
75
|
+
"""
|
|
76
|
+
# Parse model_id and version from format: model@version or just model
|
|
77
|
+
if '@' in model_id:
|
|
78
|
+
model_name, version = model_id.split('@', 1)
|
|
79
|
+
else:
|
|
80
|
+
model_name = model_id
|
|
81
|
+
version = "latest"
|
|
82
|
+
|
|
83
|
+
supabase = create_client(
|
|
84
|
+
os.getenv('SUPABASE_URL'),
|
|
85
|
+
os.getenv('SUPABASE_SERVICE_KEY')
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# If version is "latest", we need to find the actual latest version
|
|
89
|
+
if version == "latest":
|
|
90
|
+
logger.info(f"Resolving latest version for model '{model_name}'...")
|
|
91
|
+
|
|
92
|
+
# Get the latest version using our utility function
|
|
93
|
+
try:
|
|
94
|
+
version = get_latest_version(model_name)
|
|
95
|
+
logger.info(f"Resolved latest version for '{model_name}': {version}")
|
|
96
|
+
except (ValueError, RuntimeError) as e:
|
|
97
|
+
raise ValueError(f"Failed to determine latest version for model '{model_name}': {e}")
|
|
98
|
+
|
|
99
|
+
logger.info(f"Fetching manifest for model '{model_name}' version '{version}'...")
|
|
100
|
+
|
|
101
|
+
result = supabase.table('models').select('manifest').eq(
|
|
102
|
+
'model_id', model_name
|
|
103
|
+
).eq('version', version).execute()
|
|
104
|
+
|
|
105
|
+
if not result.data:
|
|
106
|
+
raise ValueError(f"Model '{model_name}' version '{version}' not found in database")
|
|
107
|
+
|
|
108
|
+
manifest = result.data[0]['manifest']
|
|
109
|
+
|
|
110
|
+
if not manifest:
|
|
111
|
+
raise ValueError(f"Model '{model_name}' version '{version}' doesn't have a manifest in database.")
|
|
112
|
+
|
|
113
|
+
return get_weights_from_manifest(manifest)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_weights_from_model_ids(model_ids: List[str]) -> List[str]:
|
|
117
|
+
"""Fetch manifests for multiple models and extract unique weights.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
model_ids: List of model identifiers with optional versions
|
|
121
|
+
(e.g., ["matrix-2", "longlive@v1.0", "mk64"])
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Deduplicated list of weight folder names from all models
|
|
125
|
+
"""
|
|
126
|
+
all_weights = set()
|
|
127
|
+
|
|
128
|
+
for model_id in model_ids:
|
|
129
|
+
try:
|
|
130
|
+
weights = get_weights_from_model_id(model_id)
|
|
131
|
+
all_weights.update(weights)
|
|
132
|
+
logger.info(f"Model '{model_id}' requires weights: {weights}")
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to fetch weights for model '{model_id}': {e}")
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
unique_weights = list(all_weights)
|
|
138
|
+
logger.info(f"Total unique weights across all models: {unique_weights}")
|
|
139
|
+
return unique_weights
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DownloadCommand:
|
|
143
|
+
@staticmethod
|
|
144
|
+
def register_subcommand(subparsers):
|
|
145
|
+
"""Register download command"""
|
|
146
|
+
download_parser = subparsers.add_parser(
|
|
147
|
+
"download",
|
|
148
|
+
help="Download model weights"
|
|
149
|
+
)
|
|
150
|
+
download_parser.add_argument(
|
|
151
|
+
"--weights",
|
|
152
|
+
nargs='+',
|
|
153
|
+
help="List of weight folder names to download"
|
|
154
|
+
)
|
|
155
|
+
download_parser.add_argument(
|
|
156
|
+
"--models",
|
|
157
|
+
nargs='+',
|
|
158
|
+
help="List of model identifiers to fetch weights from (e.g., matrix-2 longlive@v1.0). Use model@version format to specify version, defaults to 'latest'"
|
|
159
|
+
)
|
|
160
|
+
download_parser.add_argument(
|
|
161
|
+
"--no-cache",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="Force re-download even if weights exist locally"
|
|
164
|
+
)
|
|
165
|
+
download_parser.set_defaults(func=DownloadCommand)
|
|
166
|
+
|
|
167
|
+
def __init__(self, args):
|
|
168
|
+
"""Initialize command with parsed arguments"""
|
|
169
|
+
self.args = args
|
|
170
|
+
|
|
171
|
+
def run(self):
|
|
172
|
+
"""Download model weights from registry"""
|
|
173
|
+
try:
|
|
174
|
+
# Determine which route to take based on arguments
|
|
175
|
+
weights_list: Optional[List[str]] = None
|
|
176
|
+
|
|
177
|
+
if self.args.weights:
|
|
178
|
+
# Route 1: Explicit weights list provided
|
|
179
|
+
weights_list = self.args.weights
|
|
180
|
+
logger.info(f"Downloading specified weights: {weights_list}")
|
|
181
|
+
|
|
182
|
+
elif self.args.models:
|
|
183
|
+
# Route 2: Fetch from model IDs in Supabase and deduplicate
|
|
184
|
+
print(f"Fetching manifests for {len(self.args.models)} model(s)...")
|
|
185
|
+
weights_list = get_weights_from_model_ids(self.args.models)
|
|
186
|
+
print(f"Found {len(weights_list)} unique weight(s) across all models")
|
|
187
|
+
logger.info(f"Downloaded manifests for models {self.args.models}, unique weights: {weights_list}")
|
|
188
|
+
|
|
189
|
+
else:
|
|
190
|
+
# Route 3: Default - read from local manifest.json
|
|
191
|
+
logger.info("No arguments specified, reading from local manifest.json...")
|
|
192
|
+
weights_list = get_weights_from_local_manifest()
|
|
193
|
+
logger.info(f"Found weights in local manifest: {weights_list}")
|
|
194
|
+
|
|
195
|
+
if not weights_list:
|
|
196
|
+
print("No weights to download.")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Download weights in parallel
|
|
200
|
+
if self.args.no_cache:
|
|
201
|
+
print(f"Force downloading {len(weights_list)} weight folder(s) (no-cache mode)...")
|
|
202
|
+
else:
|
|
203
|
+
print(f"Downloading {len(weights_list)} weight folder(s)...")
|
|
204
|
+
weight_paths = get_weights_parallel(weights_list, no_cache=self.args.no_cache)
|
|
205
|
+
|
|
206
|
+
# Print results
|
|
207
|
+
print("\nDownload complete! Weight paths:")
|
|
208
|
+
for i, (weight_name, weight_path) in enumerate(zip(weights_list, weight_paths), 1):
|
|
209
|
+
if weight_path:
|
|
210
|
+
print(f" {i}. {weight_name}: {weight_path}")
|
|
211
|
+
else:
|
|
212
|
+
print(f" {i}. {weight_name}: FAILED")
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Download command failed: {e}")
|
|
216
|
+
print(f"Error: {e}")
|
|
217
|
+
return
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Init command implementation."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
import importlib.resources
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InitCommand:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def register_subcommand(subparsers):
|
|
10
|
+
"""Register init command"""
|
|
11
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new reactor model workspace")
|
|
12
|
+
init_parser.add_argument("name", help="Name of the model (will create directory with this name)")
|
|
13
|
+
init_parser.set_defaults(func=InitCommand)
|
|
14
|
+
|
|
15
|
+
def __init__(self, args):
|
|
16
|
+
"""Initialize with parsed arguments"""
|
|
17
|
+
self.args = args
|
|
18
|
+
|
|
19
|
+
def run(self):
|
|
20
|
+
"""Initialize a new reactor model workspace in a directory with the given name."""
|
|
21
|
+
model_name = self.args.name
|
|
22
|
+
current_dir = pathlib.Path.cwd()
|
|
23
|
+
target_dir = current_dir / model_name
|
|
24
|
+
|
|
25
|
+
# Check if directory already exists
|
|
26
|
+
if target_dir.exists():
|
|
27
|
+
print(f"Error: Directory '{model_name}' already exists")
|
|
28
|
+
print(f"Please choose a different name or remove the existing directory")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Create the model directory
|
|
32
|
+
try:
|
|
33
|
+
target_dir.mkdir()
|
|
34
|
+
print(f"Created directory: {model_name}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"Error creating directory '{model_name}': {e}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
print(f"Initializing reactor workspace in {target_dir}")
|
|
40
|
+
|
|
41
|
+
# Copy template files from the installed package
|
|
42
|
+
try:
|
|
43
|
+
import template as template_package
|
|
44
|
+
template_files = [
|
|
45
|
+
"model_template.py",
|
|
46
|
+
"manifest.json",
|
|
47
|
+
"requirements.txt",
|
|
48
|
+
"Dockerfile",
|
|
49
|
+
"README.md"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
for filename in template_files:
|
|
53
|
+
dest_path = target_dir / filename
|
|
54
|
+
if dest_path.exists():
|
|
55
|
+
print(f"Warning: {filename} already exists, skipping...")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Read template file from package resources
|
|
59
|
+
try:
|
|
60
|
+
with importlib.resources.open_text(template_package, filename) as f:
|
|
61
|
+
content = f.read()
|
|
62
|
+
|
|
63
|
+
# Write to destination
|
|
64
|
+
with open(dest_path, 'w') as f:
|
|
65
|
+
f.write(content)
|
|
66
|
+
|
|
67
|
+
print(f"Created: {filename}")
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
print(f"Warning: Template file {filename} not found in package")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
print(f"\nReactor workspace '{model_name}' initialized successfully!")
|
|
73
|
+
print(f"\nNext steps:")
|
|
74
|
+
print(f"1. cd {model_name}")
|
|
75
|
+
print("2. Edit model_template.py to implement your video model")
|
|
76
|
+
print("3. Update manifest.json with your model class, parameters, and reactor-runtime version")
|
|
77
|
+
print("4. Add any additional dependencies to requirements.txt")
|
|
78
|
+
print("5. Run 'reactor run' to start your model")
|
|
79
|
+
print(f"6. Build Docker image: docker build --build-arg GITHUB_TOKEN=<github_token> -t {model_name} .")
|
|
80
|
+
|
|
81
|
+
except ImportError:
|
|
82
|
+
print("Error: reactor_runtime package not found. Make sure it's properly installed.")
|
|
83
|
+
return
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Error initializing workspace: {e}")
|
|
86
|
+
return
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Run command implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from reactor_runtime.utils.launch import run_reactor_runtime_sync
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class RunCommand:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def register_subcommand(subparsers):
|
|
13
|
+
"""Register run command"""
|
|
14
|
+
run_parser = subparsers.add_parser("run", help="Run reactor runtime with model from manifest.json")
|
|
15
|
+
run_parser.add_argument(
|
|
16
|
+
"--deploy",
|
|
17
|
+
action="store_true",
|
|
18
|
+
help="Run the model in a proper deployment mode."
|
|
19
|
+
)
|
|
20
|
+
run_parser.add_argument(
|
|
21
|
+
"--debug","--headless",
|
|
22
|
+
action="store_true",
|
|
23
|
+
help="Run the model in a headless, debug mode."
|
|
24
|
+
)
|
|
25
|
+
run_parser.add_argument(
|
|
26
|
+
"--host",
|
|
27
|
+
type=str,
|
|
28
|
+
default="0.0.0.0",
|
|
29
|
+
help="Host to bind the FastAPI Server. Default: 0.0.0.0"
|
|
30
|
+
)
|
|
31
|
+
run_parser.add_argument(
|
|
32
|
+
"--port",
|
|
33
|
+
type=int,
|
|
34
|
+
default=8081,
|
|
35
|
+
help="FastAPI Server port. Default: 8081"
|
|
36
|
+
)
|
|
37
|
+
run_parser.add_argument(
|
|
38
|
+
"--log-level",
|
|
39
|
+
type=str,
|
|
40
|
+
default="INFO",
|
|
41
|
+
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
|
42
|
+
help="Logging level. Default: INFO"
|
|
43
|
+
)
|
|
44
|
+
run_parser.set_defaults(func=RunCommand)
|
|
45
|
+
|
|
46
|
+
def __init__(self, args):
|
|
47
|
+
"""Initialize with parsed arguments"""
|
|
48
|
+
self.args = args
|
|
49
|
+
|
|
50
|
+
def run(self):
|
|
51
|
+
"""Run the reactor runtime with the model specified in manifest.json."""
|
|
52
|
+
from ..main import verify_reactor_workspace
|
|
53
|
+
|
|
54
|
+
# Verify workspace and get manifest data
|
|
55
|
+
manifest_data = verify_reactor_workspace()
|
|
56
|
+
if manifest_data is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Extract model information from manifest
|
|
60
|
+
model_class = manifest_data["class"]
|
|
61
|
+
model_args = manifest_data.get("args", {})
|
|
62
|
+
|
|
63
|
+
if "model_name" not in manifest_data.keys():
|
|
64
|
+
print("Error: manifest.json is missing required 'model_name' field.")
|
|
65
|
+
print("Please add a 'model_name' field specifying the model name.")
|
|
66
|
+
return
|
|
67
|
+
if "model_version" not in manifest_data.keys():
|
|
68
|
+
print("Error: manifest.json is missing required 'model_version' field.")
|
|
69
|
+
print("Please add a 'model_version' field specifying the model version.")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
print(f"Starting reactor runtime...")
|
|
73
|
+
print(f"Model: {model_class}")
|
|
74
|
+
if model_args:
|
|
75
|
+
print(f"Model args: {model_args}")
|
|
76
|
+
|
|
77
|
+
runtime_serve_fn = None
|
|
78
|
+
if self.args.deploy:
|
|
79
|
+
try:
|
|
80
|
+
from reactor_runtime.context._cloud.cloud_runtime import serve
|
|
81
|
+
if os.getenv("REDIS_URL", None) is None:
|
|
82
|
+
raise KeyError("REDIS_URL environment variable is not set. Please set it to the Redis URL.")
|
|
83
|
+
runtime_serve_fn = serve
|
|
84
|
+
except KeyError as e:
|
|
85
|
+
logger.error(f"Error starting reactor runtime: {e}")
|
|
86
|
+
return
|
|
87
|
+
except ModuleNotFoundError as e:
|
|
88
|
+
logger.error("Deploy mode is not supported locally. Please use the command without --deploy.")
|
|
89
|
+
return
|
|
90
|
+
else:
|
|
91
|
+
if self.args.debug:
|
|
92
|
+
try:
|
|
93
|
+
from reactor_runtime.context.debug.debug_runtime import serve
|
|
94
|
+
runtime_serve_fn = serve
|
|
95
|
+
except ModuleNotFoundError:
|
|
96
|
+
logger.error("Debug mode is not available. Please use the command without --debug/--headless.")
|
|
97
|
+
return
|
|
98
|
+
else:
|
|
99
|
+
from reactor_runtime.context.local.local_runtime import serve
|
|
100
|
+
runtime_serve_fn = serve
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
run_reactor_runtime_sync(
|
|
104
|
+
runtime_serve_fn=runtime_serve_fn,
|
|
105
|
+
model=model_class,
|
|
106
|
+
model_args=json.dumps(model_args) if model_args else None,
|
|
107
|
+
host=self.args.host,
|
|
108
|
+
port=self.args.port,
|
|
109
|
+
log_level=self.args.log_level,
|
|
110
|
+
model_name=manifest_data["model_name"],
|
|
111
|
+
model_version=manifest_data["model_version"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
print("\nReactor runtime stopped by user.")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"Error running reactor runtime: {e}")
|
|
118
|
+
raise
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Setup command implementation.
|
|
2
|
+
|
|
3
|
+
Interactive setup for configuring Reactor weights management credentials.
|
|
4
|
+
This is for manual setup only - helps users configure their environment
|
|
5
|
+
variables for Supabase and AWS S3 access.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
class SetupCommand:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def register_subcommand(subparsers):
|
|
13
|
+
"""Register setup command"""
|
|
14
|
+
setup_parser = subparsers.add_parser("setup", help="Check reactor weights configuration")
|
|
15
|
+
setup_parser.set_defaults(func=SetupCommand)
|
|
16
|
+
|
|
17
|
+
def __init__(self, args):
|
|
18
|
+
"""Initialize command with parsed arguments"""
|
|
19
|
+
self.args = args
|
|
20
|
+
|
|
21
|
+
def run(self):
|
|
22
|
+
"""Interactive setup for reactor weights"""
|
|
23
|
+
print("Reactor weights setup")
|
|
24
|
+
print("This will help you configure Supabase and AWS credentials")
|
|
25
|
+
|
|
26
|
+
# Check current status
|
|
27
|
+
supabase_vars = {
|
|
28
|
+
"SUPABASE_URL": os.getenv("SUPABASE_URL"),
|
|
29
|
+
"SUPABASE_SERVICE_KEY": os.getenv("SUPABASE_SERVICE_KEY"),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
aws_vars = {
|
|
33
|
+
"AWS_ACCESS_KEY_ID": os.getenv("AWS_ACCESS_KEY_ID"),
|
|
34
|
+
"AWS_SECRET_ACCESS_KEY": os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Setup Supabase
|
|
38
|
+
print("\nSupabase setup:")
|
|
39
|
+
print("Go to https://supabase.com/dashboard > Settings > API")
|
|
40
|
+
|
|
41
|
+
supabase_url = input(f"Supabase URL (current: {supabase_vars['SUPABASE_URL'] or 'not set'}): ").strip()
|
|
42
|
+
supabase_service_key = input(f"Supabase service key (current: {'***' if supabase_vars['SUPABASE_SERVICE_KEY'] else 'not set'}): ").strip()
|
|
43
|
+
|
|
44
|
+
# Setup AWS
|
|
45
|
+
print("\nAWS setup:")
|
|
46
|
+
print("Go to AWS Console > IAM > Users (create user with S3 permissions)")
|
|
47
|
+
|
|
48
|
+
aws_access_key = input(f"AWS Access Key ID (current: {'***' if aws_vars['AWS_ACCESS_KEY_ID'] else 'not set'}): ").strip()
|
|
49
|
+
aws_secret_key = input(f"AWS Secret Key (current: {'***' if aws_vars['AWS_SECRET_ACCESS_KEY'] else 'not set'}): ").strip()
|
|
50
|
+
|
|
51
|
+
# Generate export commands
|
|
52
|
+
print("\nAdd these to your shell profile (.bashrc, .zshrc, etc.):")
|
|
53
|
+
|
|
54
|
+
if supabase_url:
|
|
55
|
+
print(f"export SUPABASE_URL='{supabase_url}'")
|
|
56
|
+
if supabase_service_key:
|
|
57
|
+
print(f"export SUPABASE_SERVICE_KEY='{supabase_service_key}'")
|
|
58
|
+
if aws_access_key:
|
|
59
|
+
print(f"export AWS_ACCESS_KEY_ID='{aws_access_key}'")
|
|
60
|
+
if aws_secret_key:
|
|
61
|
+
print(f"export AWS_SECRET_ACCESS_KEY='{aws_secret_key}'")
|
|
62
|
+
|
|
63
|
+
print("\nRun 'source ~/.bashrc' (or restart terminal) to apply changes")
|
|
64
|
+
|
|
65
|
+
# Test connections if user entered values
|
|
66
|
+
if any([supabase_url, supabase_service_key, aws_access_key, aws_secret_key]):
|
|
67
|
+
print("\nTesting connections with provided credentials...")
|
|
68
|
+
self._test_connections(supabase_url, supabase_service_key, aws_access_key, aws_secret_key)
|
|
69
|
+
|
|
70
|
+
def _test_connections(self, supabase_url=None, supabase_service_key=None, aws_access_key=None, aws_secret_key=None):
|
|
71
|
+
"""Test connections with provided credentials"""
|
|
72
|
+
|
|
73
|
+
# Test Supabase with provided values or environment
|
|
74
|
+
try:
|
|
75
|
+
from supabase import create_client
|
|
76
|
+
url = supabase_url or os.getenv('SUPABASE_URL')
|
|
77
|
+
key = supabase_service_key or os.getenv('SUPABASE_SERVICE_KEY')
|
|
78
|
+
|
|
79
|
+
if url and key:
|
|
80
|
+
supabase = create_client(url, key)
|
|
81
|
+
supabase.table('models').select('count').execute()
|
|
82
|
+
print("Supabase: connection successful")
|
|
83
|
+
else:
|
|
84
|
+
print("Supabase: skipped (no credentials provided)")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Supabase: connection failed - {e}")
|
|
87
|
+
|
|
88
|
+
# Test AWS with provided values or environment
|
|
89
|
+
try:
|
|
90
|
+
import boto3
|
|
91
|
+
access_key = aws_access_key or os.getenv('AWS_ACCESS_KEY_ID')
|
|
92
|
+
secret_key = aws_secret_key or os.getenv('AWS_SECRET_ACCESS_KEY')
|
|
93
|
+
|
|
94
|
+
if access_key and secret_key:
|
|
95
|
+
s3 = boto3.client('s3',
|
|
96
|
+
aws_access_key_id=access_key,
|
|
97
|
+
aws_secret_access_key=secret_key)
|
|
98
|
+
s3.list_buckets()
|
|
99
|
+
print("AWS S3: connection successful")
|
|
100
|
+
else:
|
|
101
|
+
print("AWS S3: skipped (no credentials provided)")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"AWS S3: connection failed - {e}")
|