synth-ai 0.2.9.dev4__py3-none-any.whl → 0.2.9.dev7__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.
Potentially problematic release.
This version of synth-ai might be problematic. Click here for more details.
- examples/common_old/backend.py +0 -1
- examples/crafter_debug_render.py +15 -6
- examples/evals_old/compare_models.py +1 -0
- examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +6 -2
- examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +4 -4
- examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +4 -3
- examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +6 -2
- examples/finetuning_old/synth_qwen_v1/finetune.py +1 -1
- examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +4 -4
- examples/finetuning_old/synth_qwen_v1/infer.py +1 -2
- examples/finetuning_old/synth_qwen_v1/poll.py +4 -2
- examples/finetuning_old/synth_qwen_v1/prepare_data.py +8 -8
- examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +5 -4
- examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +11 -8
- examples/finetuning_old/synth_qwen_v1/run_ft_job.py +17 -12
- examples/finetuning_old/synth_qwen_v1/upload_data.py +1 -1
- examples/finetuning_old/synth_qwen_v1/util.py +7 -2
- examples/rl/configs/eval_base_qwen.toml +1 -1
- examples/rl/configs/rl_from_base_qwen17.toml +1 -1
- examples/rl/download_dataset.py +26 -10
- examples/rl/run_eval.py +17 -15
- examples/rl/run_rl_and_save.py +24 -7
- examples/rl/task_app/math_single_step.py +128 -11
- examples/rl/task_app/math_task_app.py +11 -3
- examples/rl_old/task_app.py +222 -53
- examples/warming_up_to_rl/analyze_trace_db.py +7 -5
- examples/warming_up_to_rl/export_trace_sft.py +141 -16
- examples/warming_up_to_rl/groq_test.py +11 -4
- examples/warming_up_to_rl/manage_secrets.py +15 -6
- examples/warming_up_to_rl/readme.md +9 -2
- examples/warming_up_to_rl/run_eval.py +108 -30
- examples/warming_up_to_rl/run_fft_and_save.py +128 -52
- examples/warming_up_to_rl/run_local_rollout.py +87 -36
- examples/warming_up_to_rl/run_local_rollout_modal.py +113 -25
- examples/warming_up_to_rl/run_local_rollout_parallel.py +80 -16
- examples/warming_up_to_rl/run_local_rollout_traced.py +125 -20
- examples/warming_up_to_rl/run_rl_and_save.py +31 -7
- examples/warming_up_to_rl/run_rollout_remote.py +37 -10
- examples/warming_up_to_rl/task_app/grpo_crafter.py +90 -27
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +9 -27
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +46 -108
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +50 -17
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +35 -21
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +8 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +29 -26
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +17 -13
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +106 -63
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +82 -84
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +76 -59
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +43 -49
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +5 -15
- synth_ai/__init__.py +1 -0
- synth_ai/api/train/builders.py +34 -10
- synth_ai/api/train/cli.py +172 -32
- synth_ai/api/train/config_finder.py +59 -4
- synth_ai/api/train/env_resolver.py +32 -14
- synth_ai/api/train/pollers.py +11 -3
- synth_ai/api/train/task_app.py +4 -1
- synth_ai/api/train/utils.py +20 -4
- synth_ai/cli/__init__.py +11 -4
- synth_ai/cli/balance.py +1 -1
- synth_ai/cli/demo.py +19 -5
- synth_ai/cli/rl_demo.py +75 -16
- synth_ai/cli/root.py +116 -37
- synth_ai/cli/task_apps.py +1286 -170
- synth_ai/cli/traces.py +1 -0
- synth_ai/cli/turso.py +73 -0
- synth_ai/core/experiment.py +0 -2
- synth_ai/demo_registry.py +67 -30
- synth_ai/demos/core/cli.py +493 -164
- synth_ai/demos/demo_task_apps/core.py +50 -6
- synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +36 -28
- synth_ai/demos/demo_task_apps/math/_common.py +1 -2
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +0 -2
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +168 -65
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
- synth_ai/environments/examples/bandit/engine.py +12 -4
- synth_ai/environments/examples/bandit/taskset.py +4 -4
- synth_ai/environments/reproducibility/tree.py +3 -1
- synth_ai/environments/service/core_routes.py +6 -2
- synth_ai/evals/base.py +0 -2
- synth_ai/experimental/synth_oss.py +11 -12
- synth_ai/handshake.py +3 -1
- synth_ai/http_client.py +31 -7
- synth_ai/inference/__init__.py +0 -2
- synth_ai/inference/client.py +8 -4
- synth_ai/jobs/client.py +40 -10
- synth_ai/learning/client.py +33 -8
- synth_ai/learning/config.py +0 -2
- synth_ai/learning/constants.py +0 -2
- synth_ai/learning/ft_client.py +6 -3
- synth_ai/learning/health.py +9 -2
- synth_ai/learning/jobs.py +17 -5
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +1 -3
- synth_ai/learning/prompts/random_search.py +4 -1
- synth_ai/learning/prompts/run_random_search_banking77.py +6 -1
- synth_ai/learning/rl_client.py +42 -14
- synth_ai/learning/sse.py +0 -2
- synth_ai/learning/validators.py +6 -2
- synth_ai/lm/caching/ephemeral.py +1 -3
- synth_ai/lm/core/exceptions.py +0 -2
- synth_ai/lm/core/main.py +13 -1
- synth_ai/lm/core/synth_models.py +0 -1
- synth_ai/lm/core/vendor_clients.py +4 -2
- synth_ai/lm/overrides.py +2 -2
- synth_ai/lm/vendors/core/anthropic_api.py +7 -7
- synth_ai/lm/vendors/core/openai_api.py +2 -0
- synth_ai/lm/vendors/openai_standard.py +3 -1
- synth_ai/lm/vendors/openai_standard_responses.py +6 -3
- synth_ai/lm/vendors/supported/custom_endpoint.py +1 -3
- synth_ai/lm/vendors/synth_client.py +37 -10
- synth_ai/rl/__init__.py +0 -1
- synth_ai/rl/contracts.py +0 -2
- synth_ai/rl/env_keys.py +6 -1
- synth_ai/task/__init__.py +1 -0
- synth_ai/task/apps/__init__.py +11 -11
- synth_ai/task/auth.py +29 -17
- synth_ai/task/client.py +3 -1
- synth_ai/task/contracts.py +1 -0
- synth_ai/task/datasets.py +3 -1
- synth_ai/task/errors.py +3 -2
- synth_ai/task/health.py +0 -2
- synth_ai/task/json.py +0 -1
- synth_ai/task/proxy.py +2 -5
- synth_ai/task/rubrics.py +9 -3
- synth_ai/task/server.py +31 -5
- synth_ai/task/tracing_utils.py +8 -3
- synth_ai/task/validators.py +0 -1
- synth_ai/task/vendors.py +0 -1
- synth_ai/tracing_v3/db_config.py +26 -1
- synth_ai/tracing_v3/decorators.py +1 -0
- synth_ai/tracing_v3/examples/basic_usage.py +3 -2
- synth_ai/tracing_v3/hooks.py +2 -0
- synth_ai/tracing_v3/replica_sync.py +1 -0
- synth_ai/tracing_v3/session_tracer.py +24 -3
- synth_ai/tracing_v3/storage/base.py +4 -1
- synth_ai/tracing_v3/storage/factory.py +0 -1
- synth_ai/tracing_v3/turso/manager.py +102 -38
- synth_ai/tracing_v3/turso/models.py +4 -1
- synth_ai/tracing_v3/utils.py +1 -0
- synth_ai/v0/tracing/upload.py +32 -135
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/METADATA +1 -1
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/RECORD +154 -156
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_stepwise_rewards.py +0 -58
- synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
- synth_ai/install_sqld.sh +0 -40
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/top_level.txt +0 -0
synth_ai/demos/core/cli.py
CHANGED
|
@@ -45,7 +45,17 @@ def _is_modal_public_url(u: str) -> bool:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def cmd_setup(_args: argparse.Namespace) -> int:
|
|
48
|
-
#
|
|
48
|
+
# Change to demo directory if stored
|
|
49
|
+
demo_dir = demo_core.load_demo_dir()
|
|
50
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
51
|
+
os.chdir(demo_dir)
|
|
52
|
+
print(f"Using demo directory: {demo_dir}")
|
|
53
|
+
|
|
54
|
+
# 1) Try to fetch keys from frontend; fall back to manual input if fetch fails
|
|
55
|
+
synth_key = ""
|
|
56
|
+
rl_env_key = ""
|
|
57
|
+
org_name = "this organization"
|
|
58
|
+
|
|
49
59
|
try:
|
|
50
60
|
print("\n⏳ Connecting SDK to your browser session…")
|
|
51
61
|
res = run_handshake()
|
|
@@ -54,25 +64,52 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
54
64
|
keys = res.get("keys") or {}
|
|
55
65
|
synth_key = str(keys.get("synth") or "").strip()
|
|
56
66
|
rl_env_key = str(keys.get("rl_env") or "").strip()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
org_name = org.get("name") or "this organization"
|
|
68
|
+
print(f"✅ Connected to {org_name}!")
|
|
69
|
+
except (HandshakeError, Exception) as e:
|
|
70
|
+
print(f"⚠️ Failed to fetch keys from frontend: {e}")
|
|
71
|
+
print("Falling back to manual entry...")
|
|
72
|
+
|
|
73
|
+
# Prompt for manual input if any key is missing
|
|
74
|
+
if not synth_key:
|
|
75
|
+
try:
|
|
76
|
+
synth_key = input(
|
|
77
|
+
"Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
|
|
78
|
+
).strip()
|
|
79
|
+
except (EOFError, KeyboardInterrupt):
|
|
80
|
+
print("\nSetup cancelled.")
|
|
81
|
+
return 1
|
|
82
|
+
if not synth_key:
|
|
83
|
+
print("Synth API key is required.")
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
if not rl_env_key:
|
|
87
|
+
try:
|
|
88
|
+
rl_env_key = input(
|
|
89
|
+
"Failed to fetch your RL Environment API key. Please enter your RL Environment API key here:\n> "
|
|
90
|
+
).strip()
|
|
91
|
+
except (EOFError, KeyboardInterrupt):
|
|
92
|
+
print("\nSetup cancelled.")
|
|
93
|
+
return 1
|
|
94
|
+
if not rl_env_key:
|
|
95
|
+
print("RL Environment API key is required.")
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
# Persist both keys to .env
|
|
99
|
+
dotenv_path = demo_core.persist_dotenv_values(
|
|
100
|
+
{
|
|
61
101
|
"SYNTH_API_KEY": synth_key,
|
|
62
102
|
"ENVIRONMENT_API_KEY": rl_env_key,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return 1
|
|
69
|
-
except Exception as e:
|
|
70
|
-
print(f"Unexpected handshake error: {e}")
|
|
71
|
-
return 1
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Store .env path for subsequent commands
|
|
107
|
+
demo_core.persist_env_file_path(dotenv_path)
|
|
72
108
|
|
|
73
109
|
# 2) Reload env after handshake to pick up values from .env (suppress env prints)
|
|
74
110
|
import io
|
|
75
111
|
import contextlib
|
|
112
|
+
|
|
76
113
|
_buf = io.StringIO()
|
|
77
114
|
with contextlib.redirect_stdout(_buf):
|
|
78
115
|
env = demo_core.load_env()
|
|
@@ -95,16 +132,18 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
95
132
|
needs_lookup = True
|
|
96
133
|
if not needs_lookup:
|
|
97
134
|
return
|
|
98
|
-
code, out = _popen_capture(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
code, out = _popen_capture(
|
|
136
|
+
[
|
|
137
|
+
"uv",
|
|
138
|
+
"run",
|
|
139
|
+
"python",
|
|
140
|
+
"-m",
|
|
141
|
+
"modal",
|
|
142
|
+
"app",
|
|
143
|
+
"url",
|
|
144
|
+
env.task_app_name,
|
|
145
|
+
]
|
|
146
|
+
)
|
|
108
147
|
if code != 0 or not out:
|
|
109
148
|
return
|
|
110
149
|
new_url = ""
|
|
@@ -137,12 +176,15 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
137
176
|
ok_backend = False
|
|
138
177
|
ok_task = False
|
|
139
178
|
if env.dev_backend_url:
|
|
140
|
-
api = env.dev_backend_url.rstrip("/") + (
|
|
179
|
+
api = env.dev_backend_url.rstrip("/") + (
|
|
180
|
+
"" if env.dev_backend_url.endswith("/api") else "/api"
|
|
181
|
+
)
|
|
141
182
|
ok_backend = demo_core.assert_http_ok(api + "/health", method="GET")
|
|
142
183
|
# Intentionally suppress backend health print for concise output
|
|
143
184
|
if env.task_app_base_url:
|
|
144
|
-
ok_task = demo_core.assert_http_ok(
|
|
145
|
-
|
|
185
|
+
ok_task = demo_core.assert_http_ok(
|
|
186
|
+
env.task_app_base_url.rstrip("/") + "/health", method="GET"
|
|
187
|
+
) or demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
|
|
146
188
|
# Intentionally suppress task app health print
|
|
147
189
|
else:
|
|
148
190
|
print("\nSet your task app URL by running:\nuvx synth-ai rl_demo deploy\n")
|
|
@@ -150,13 +192,19 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
150
192
|
# Omit uv version print to keep output concise
|
|
151
193
|
|
|
152
194
|
# Keep exit code neutral; not all checks are critical for pairing
|
|
195
|
+
print(f"\nKeys saved to: {dotenv_path}")
|
|
153
196
|
return 0
|
|
154
197
|
|
|
155
198
|
|
|
156
|
-
def _popen_capture(
|
|
199
|
+
def _popen_capture(
|
|
200
|
+
cmd: list[str], cwd: str | None = None, env: dict | None = None
|
|
201
|
+
) -> tuple[int, str]:
|
|
157
202
|
import subprocess
|
|
203
|
+
|
|
158
204
|
try:
|
|
159
|
-
proc = subprocess.Popen(
|
|
205
|
+
proc = subprocess.Popen(
|
|
206
|
+
cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
207
|
+
)
|
|
160
208
|
out, _ = proc.communicate()
|
|
161
209
|
return int(proc.returncode or 0), out or ""
|
|
162
210
|
except Exception as e:
|
|
@@ -200,7 +248,9 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
|
|
|
200
248
|
return int(proc.returncode or 0)
|
|
201
249
|
|
|
202
250
|
|
|
203
|
-
def _popen_stream_capture(
|
|
251
|
+
def _popen_stream_capture(
|
|
252
|
+
cmd: list[str], cwd: str | None = None, env: dict | None = None
|
|
253
|
+
) -> tuple[int, str]:
|
|
204
254
|
"""Stream subprocess output to stdout and also capture it into a buffer."""
|
|
205
255
|
import subprocess
|
|
206
256
|
import threading
|
|
@@ -251,7 +301,19 @@ def _find_asgi_apps(root: Path) -> list[Path]:
|
|
|
251
301
|
- "@modal.asgi_app()"
|
|
252
302
|
"""
|
|
253
303
|
results: list[Path] = []
|
|
254
|
-
skip_dirs = {
|
|
304
|
+
skip_dirs = {
|
|
305
|
+
".git",
|
|
306
|
+
".hg",
|
|
307
|
+
".svn",
|
|
308
|
+
"node_modules",
|
|
309
|
+
"dist",
|
|
310
|
+
"build",
|
|
311
|
+
"__pycache__",
|
|
312
|
+
".ruff_cache",
|
|
313
|
+
".mypy_cache",
|
|
314
|
+
"venv",
|
|
315
|
+
".venv",
|
|
316
|
+
}
|
|
255
317
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
256
318
|
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
|
|
257
319
|
for name in filenames:
|
|
@@ -265,16 +327,20 @@ def _find_asgi_apps(root: Path) -> list[Path]:
|
|
|
265
327
|
results.append(path)
|
|
266
328
|
except Exception:
|
|
267
329
|
continue
|
|
330
|
+
|
|
268
331
|
# Stable order: prioritize files under synth_demo/ first, then alphabetical
|
|
269
332
|
def _priority(p: Path) -> tuple[int, str]:
|
|
270
333
|
rel = str(p.resolve())
|
|
271
334
|
in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
|
|
272
335
|
return (0 if in_demo else 1, rel)
|
|
336
|
+
|
|
273
337
|
results.sort(key=_priority)
|
|
274
338
|
return results
|
|
275
339
|
|
|
276
340
|
|
|
277
|
-
def _prompt_value(
|
|
341
|
+
def _prompt_value(
|
|
342
|
+
label: str, default: str | int | float, cast: Callable[[str], Any] | None = None
|
|
343
|
+
) -> Any:
|
|
278
344
|
prompt = f"{label} [{default}]: "
|
|
279
345
|
try:
|
|
280
346
|
raw = input(prompt).strip()
|
|
@@ -293,7 +359,19 @@ def _prompt_value(label: str, default: str | int | float, cast: Callable[[str],
|
|
|
293
359
|
|
|
294
360
|
def _find_vllm_tomls(root: Path) -> list[Path]:
|
|
295
361
|
results: list[Path] = []
|
|
296
|
-
skip_dirs = {
|
|
362
|
+
skip_dirs = {
|
|
363
|
+
".git",
|
|
364
|
+
".hg",
|
|
365
|
+
".svn",
|
|
366
|
+
"node_modules",
|
|
367
|
+
"dist",
|
|
368
|
+
"build",
|
|
369
|
+
"__pycache__",
|
|
370
|
+
".ruff_cache",
|
|
371
|
+
".mypy_cache",
|
|
372
|
+
"venv",
|
|
373
|
+
".venv",
|
|
374
|
+
}
|
|
297
375
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
298
376
|
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
|
|
299
377
|
for name in filenames:
|
|
@@ -313,7 +391,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
313
391
|
default_path = os.path.join(os.getcwd(), "demo_config.toml")
|
|
314
392
|
while True:
|
|
315
393
|
try:
|
|
316
|
-
destination =
|
|
394
|
+
destination = (
|
|
395
|
+
input(f"Path to save new config [{default_path}]: ").strip() or default_path
|
|
396
|
+
)
|
|
317
397
|
except Exception:
|
|
318
398
|
destination = default_path
|
|
319
399
|
destination = os.path.abspath(destination)
|
|
@@ -322,7 +402,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
322
402
|
continue
|
|
323
403
|
if os.path.exists(destination):
|
|
324
404
|
try:
|
|
325
|
-
overwrite =
|
|
405
|
+
overwrite = (
|
|
406
|
+
input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
|
|
407
|
+
)
|
|
326
408
|
except Exception:
|
|
327
409
|
overwrite = "n"
|
|
328
410
|
if not overwrite.startswith("y"):
|
|
@@ -334,7 +416,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
334
416
|
model_name = _prompt_value("Model name", "Qwen/Qwen3-0.6B")
|
|
335
417
|
compute_gpu_type = _prompt_value("Compute GPU type", "H100")
|
|
336
418
|
compute_gpu_count = _prompt_value("Compute GPU count", 4, int)
|
|
337
|
-
topology_gpu_type = _prompt_value(
|
|
419
|
+
topology_gpu_type = _prompt_value(
|
|
420
|
+
"Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}"
|
|
421
|
+
)
|
|
338
422
|
gpus_for_vllm = _prompt_value("Topology gpus_for_vllm", 2, int)
|
|
339
423
|
gpus_for_training = _prompt_value("Topology gpus_for_training", 1, int)
|
|
340
424
|
tensor_parallel = _prompt_value("Topology tensor_parallel", 2, int)
|
|
@@ -352,8 +436,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
352
436
|
task_url_default = env.task_app_base_url or ""
|
|
353
437
|
services_task_url = _prompt_value("services.task_url", task_url_default)
|
|
354
438
|
|
|
355
|
-
template =
|
|
356
|
-
|
|
439
|
+
template = (
|
|
440
|
+
textwrap.dedent(
|
|
441
|
+
f"""\
|
|
357
442
|
# Crafter online RL training configuration (research local copy)
|
|
358
443
|
|
|
359
444
|
[model]
|
|
@@ -495,7 +580,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
495
580
|
[services]
|
|
496
581
|
task_url = \"{services_task_url}\"
|
|
497
582
|
"""
|
|
498
|
-
|
|
583
|
+
).strip()
|
|
584
|
+
+ "\n"
|
|
585
|
+
)
|
|
499
586
|
|
|
500
587
|
with open(destination, "w", encoding="utf-8") as fh:
|
|
501
588
|
fh.write(template)
|
|
@@ -514,7 +601,11 @@ def _select_or_create_config(explicit: str | None, env: DemoEnv) -> str:
|
|
|
514
601
|
discovered = _find_vllm_tomls(search_root)
|
|
515
602
|
|
|
516
603
|
extras: list[Path] = []
|
|
517
|
-
packaged = Path(
|
|
604
|
+
packaged = Path(
|
|
605
|
+
os.path.abspath(
|
|
606
|
+
os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")
|
|
607
|
+
)
|
|
608
|
+
)
|
|
518
609
|
extras.append(packaged)
|
|
519
610
|
home_cfg = Path(os.path.expanduser("~/.synth-ai/demo_config.toml"))
|
|
520
611
|
extras.append(home_cfg)
|
|
@@ -560,29 +651,36 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
560
651
|
|
|
561
652
|
env_key = (env.env_api_key or "").strip()
|
|
562
653
|
if not env_key:
|
|
563
|
-
raise RuntimeError(
|
|
654
|
+
raise RuntimeError(
|
|
655
|
+
f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first."
|
|
656
|
+
)
|
|
564
657
|
|
|
565
658
|
task_url = env.task_app_base_url
|
|
566
659
|
if not task_url or not _is_modal_public_url(task_url):
|
|
567
660
|
resolved = ""
|
|
568
661
|
if env.task_app_name:
|
|
569
662
|
try:
|
|
570
|
-
choice =
|
|
571
|
-
f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
|
|
572
|
-
|
|
663
|
+
choice = (
|
|
664
|
+
input(f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: ")
|
|
665
|
+
.strip()
|
|
666
|
+
.lower()
|
|
667
|
+
or "y"
|
|
668
|
+
)
|
|
573
669
|
except Exception:
|
|
574
670
|
choice = "y"
|
|
575
671
|
if choice.startswith("y"):
|
|
576
|
-
code, out = _popen_capture(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
672
|
+
code, out = _popen_capture(
|
|
673
|
+
[
|
|
674
|
+
"uv",
|
|
675
|
+
"run",
|
|
676
|
+
"python",
|
|
677
|
+
"-m",
|
|
678
|
+
"modal",
|
|
679
|
+
"app",
|
|
680
|
+
"url",
|
|
681
|
+
env.task_app_name,
|
|
682
|
+
]
|
|
683
|
+
)
|
|
586
684
|
if code == 0 and out:
|
|
587
685
|
for tok in out.split():
|
|
588
686
|
if _is_modal_public_url(tok):
|
|
@@ -591,7 +689,9 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
591
689
|
if not resolved:
|
|
592
690
|
print(f"[{label}] Task app URL not configured or not a valid Modal public URL.")
|
|
593
691
|
print("Examples: https://<app-name>-fastapi-app.modal.run")
|
|
594
|
-
entered = input(
|
|
692
|
+
entered = input(
|
|
693
|
+
"Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
|
|
694
|
+
).strip()
|
|
595
695
|
if not entered or not _is_modal_public_url(entered):
|
|
596
696
|
raise RuntimeError(f"[{label}] Valid Task App URL is required.")
|
|
597
697
|
task_url = entered.rstrip("/")
|
|
@@ -608,11 +708,13 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
608
708
|
demo_core.persist_task_url(task_url, name=app_name)
|
|
609
709
|
|
|
610
710
|
demo_core.persist_task_url(task_url, name=app_name)
|
|
611
|
-
demo_core.persist_dotenv_values(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
711
|
+
demo_core.persist_dotenv_values(
|
|
712
|
+
{
|
|
713
|
+
"TASK_APP_BASE_URL": task_url,
|
|
714
|
+
"TASK_APP_NAME": app_name,
|
|
715
|
+
"TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
|
|
716
|
+
}
|
|
717
|
+
)
|
|
616
718
|
|
|
617
719
|
if synth_key:
|
|
618
720
|
os.environ["SYNTH_API_KEY"] = synth_key
|
|
@@ -667,6 +769,12 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
667
769
|
|
|
668
770
|
|
|
669
771
|
def cmd_deploy(args: argparse.Namespace) -> int:
|
|
772
|
+
# Change to demo directory if stored
|
|
773
|
+
demo_dir = demo_core.load_demo_dir()
|
|
774
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
775
|
+
os.chdir(demo_dir)
|
|
776
|
+
print(f"Using demo directory: {demo_dir}")
|
|
777
|
+
|
|
670
778
|
env = demo_core.load_env()
|
|
671
779
|
os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
|
|
672
780
|
cwd_env_path = os.path.join(os.getcwd(), ".env")
|
|
@@ -677,12 +785,22 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
677
785
|
if args.local:
|
|
678
786
|
print("Starting local Task App…")
|
|
679
787
|
import subprocess
|
|
680
|
-
|
|
681
|
-
|
|
788
|
+
|
|
789
|
+
subprocess.Popen(
|
|
790
|
+
[
|
|
791
|
+
sys.executable,
|
|
792
|
+
"-c",
|
|
793
|
+
"from synth_ai.demos.demo_task_apps.math.app import run; run()",
|
|
794
|
+
],
|
|
795
|
+
stdout=sys.stdout,
|
|
796
|
+
stderr=sys.stderr,
|
|
797
|
+
)
|
|
682
798
|
target = "http://127.0.0.1:8080"
|
|
683
799
|
app_name = ""
|
|
684
800
|
for _ in range(30):
|
|
685
|
-
if demo_core.assert_http_ok(
|
|
801
|
+
if demo_core.assert_http_ok(
|
|
802
|
+
target + "/health", method="GET"
|
|
803
|
+
) or demo_core.assert_http_ok(target, method="GET"):
|
|
686
804
|
url = target
|
|
687
805
|
break
|
|
688
806
|
time.sleep(1)
|
|
@@ -707,7 +825,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
707
825
|
rel = os.path.relpath(str(pth), os.getcwd())
|
|
708
826
|
print(f" [{idx}] {rel}")
|
|
709
827
|
try:
|
|
710
|
-
sel =
|
|
828
|
+
sel = (
|
|
829
|
+
input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
|
|
830
|
+
)
|
|
711
831
|
except Exception:
|
|
712
832
|
sel = "1"
|
|
713
833
|
try:
|
|
@@ -719,6 +839,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
719
839
|
if not app_path and args.script:
|
|
720
840
|
# Legacy script fallback if user supplied --script explicitly
|
|
721
841
|
from synth_ai.demos.demo_task_apps.math.deploy_modal import deploy as modal_deploy
|
|
842
|
+
|
|
722
843
|
url = modal_deploy(script_path=args.script, env_api_key=env.env_api_key)
|
|
723
844
|
if args.name:
|
|
724
845
|
app_name = args.name
|
|
@@ -750,9 +871,12 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
750
871
|
env_key: str | None = existing_env_key or None
|
|
751
872
|
if existing_env_key:
|
|
752
873
|
try:
|
|
753
|
-
reuse_choice =
|
|
754
|
-
"Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: "
|
|
755
|
-
|
|
874
|
+
reuse_choice = (
|
|
875
|
+
input("Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: ")
|
|
876
|
+
.strip()
|
|
877
|
+
.lower()
|
|
878
|
+
or "y"
|
|
879
|
+
)
|
|
756
880
|
except Exception:
|
|
757
881
|
reuse_choice = "y"
|
|
758
882
|
if not reuse_choice.startswith("y"):
|
|
@@ -770,35 +894,50 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
770
894
|
print("[deploy] Minted new ENVIRONMENT_API_KEY")
|
|
771
895
|
elif env_key:
|
|
772
896
|
os.environ["ENVIRONMENT_API_KEY"] = env_key
|
|
773
|
-
|
|
897
|
+
|
|
774
898
|
# Optionally upload the new key to the backend using sealed box helper
|
|
775
899
|
backend_base = (env.dev_backend_url or "").rstrip("/")
|
|
776
|
-
synth_key = (
|
|
900
|
+
synth_key = (
|
|
901
|
+
env.synth_api_key
|
|
902
|
+
or os.environ.get("SYNTH_API_KEY")
|
|
903
|
+
or local_env.get("SYNTH_API_KEY")
|
|
904
|
+
or ""
|
|
905
|
+
).strip()
|
|
777
906
|
if backend_base and synth_key:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
907
|
+
# Pass a base WITHOUT trailing /api to setup_environment_api_key,
|
|
908
|
+
# since it appends /api/v1/... internally.
|
|
909
|
+
non_api_base = (
|
|
910
|
+
backend_base[:-4] if backend_base.endswith("/api") else backend_base
|
|
911
|
+
)
|
|
912
|
+
try:
|
|
913
|
+
choice = (
|
|
914
|
+
input(f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: ")
|
|
915
|
+
.strip()
|
|
916
|
+
.lower()
|
|
917
|
+
or "y"
|
|
918
|
+
)
|
|
919
|
+
except Exception:
|
|
920
|
+
choice = "y"
|
|
921
|
+
if choice.startswith("y"):
|
|
781
922
|
try:
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
from synth_ai.rl.env_keys import setup_environment_api_key
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
|
|
923
|
+
print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
|
|
924
|
+
from synth_ai.rl.env_keys import setup_environment_api_key
|
|
925
|
+
|
|
926
|
+
setup_environment_api_key(non_api_base, synth_key, token=env_key)
|
|
927
|
+
print("[deploy] Backend sealed-box upload complete.")
|
|
928
|
+
except Exception as upload_err:
|
|
929
|
+
print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
|
|
930
|
+
print(
|
|
931
|
+
'Hint: run `uvx python -c "from synth_ai.rl.env_keys import setup_environment_api_key as s;'
|
|
932
|
+
" s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
synth_key = (
|
|
936
|
+
env.synth_api_key
|
|
937
|
+
or os.environ.get("SYNTH_API_KEY")
|
|
938
|
+
or local_env.get("SYNTH_API_KEY")
|
|
939
|
+
or ""
|
|
940
|
+
).strip()
|
|
802
941
|
if not synth_key:
|
|
803
942
|
synth_key = input("Enter SYNTH_API_KEY for deployment (required): ").strip()
|
|
804
943
|
if not synth_key:
|
|
@@ -809,7 +948,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
809
948
|
env.synth_api_key = synth_key
|
|
810
949
|
os.environ["SYNTH_API_KEY"] = synth_key
|
|
811
950
|
|
|
812
|
-
openai_key = (
|
|
951
|
+
openai_key = (
|
|
952
|
+
os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or ""
|
|
953
|
+
).strip()
|
|
813
954
|
if not openai_key:
|
|
814
955
|
openai_key = input(
|
|
815
956
|
"Enter your OpenAI API key, found at https://platform.openai.com/api-keys\n> "
|
|
@@ -821,8 +962,20 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
821
962
|
local_env["OPENAI_API_KEY"] = openai_key
|
|
822
963
|
os.environ["OPENAI_API_KEY"] = openai_key
|
|
823
964
|
|
|
824
|
-
deploy_cmd = [
|
|
825
|
-
|
|
965
|
+
deploy_cmd = [
|
|
966
|
+
"uv",
|
|
967
|
+
"run",
|
|
968
|
+
"python",
|
|
969
|
+
"-m",
|
|
970
|
+
"modal",
|
|
971
|
+
"deploy",
|
|
972
|
+
"--name",
|
|
973
|
+
name_in,
|
|
974
|
+
app_path,
|
|
975
|
+
]
|
|
976
|
+
print(
|
|
977
|
+
"\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n"
|
|
978
|
+
)
|
|
826
979
|
code, deploy_logs = _popen_stream_capture(deploy_cmd)
|
|
827
980
|
if code != 0:
|
|
828
981
|
raise RuntimeError(f"modal deploy failed (exit {code})")
|
|
@@ -830,6 +983,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
830
983
|
if not url:
|
|
831
984
|
try:
|
|
832
985
|
import re as _re
|
|
986
|
+
|
|
833
987
|
m_all = _re.findall(r"https?://[^\s]+\.modal\.run", deploy_logs or "")
|
|
834
988
|
if m_all:
|
|
835
989
|
url = m_all[-1].strip().rstrip("/")
|
|
@@ -844,7 +998,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
844
998
|
break
|
|
845
999
|
# Fallback: try reading recent Modal logs for the app to find a URL line
|
|
846
1000
|
if not url:
|
|
847
|
-
code3, out3 = _popen_capture(
|
|
1001
|
+
code3, out3 = _popen_capture(
|
|
1002
|
+
["uv", "run", "python", "-m", "modal", "app", "list"]
|
|
1003
|
+
)
|
|
848
1004
|
if code3 == 0 and out3:
|
|
849
1005
|
for line in out3.splitlines():
|
|
850
1006
|
if name_in in line:
|
|
@@ -857,7 +1013,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
857
1013
|
# Prompt user if still no valid URL
|
|
858
1014
|
if not url:
|
|
859
1015
|
print("\nCould not auto-detect a public Modal URL for the app.")
|
|
860
|
-
entered = input(
|
|
1016
|
+
entered = input(
|
|
1017
|
+
"Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: "
|
|
1018
|
+
).strip()
|
|
861
1019
|
if entered and _is_modal_public_url(entered):
|
|
862
1020
|
url = entered.rstrip("/")
|
|
863
1021
|
if not url:
|
|
@@ -885,8 +1043,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
885
1043
|
print(f"Deploy error: {e}")
|
|
886
1044
|
return 2
|
|
887
1045
|
|
|
888
|
-
|
|
889
|
-
|
|
1046
|
+
print(
|
|
1047
|
+
"`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches."
|
|
1048
|
+
)
|
|
890
1049
|
env = demo_core.load_env()
|
|
891
1050
|
synth_key = (env.synth_api_key or "").strip()
|
|
892
1051
|
if not synth_key:
|
|
@@ -919,40 +1078,62 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
919
1078
|
|
|
920
1079
|
|
|
921
1080
|
def _ensure_modal_installed() -> None:
|
|
922
|
-
"""Install the modal package if it is not already available."""
|
|
1081
|
+
"""Install the modal package if it is not already available and check authentication."""
|
|
923
1082
|
|
|
1083
|
+
# Check if modal is installed
|
|
1084
|
+
modal_installed = False
|
|
924
1085
|
try:
|
|
925
1086
|
import importlib.util as _iu
|
|
926
1087
|
|
|
927
1088
|
if _iu.find_spec("modal") is not None:
|
|
928
|
-
|
|
929
|
-
return
|
|
1089
|
+
modal_installed = True
|
|
930
1090
|
except Exception:
|
|
931
1091
|
pass
|
|
932
1092
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1093
|
+
# Install modal if needed
|
|
1094
|
+
if not modal_installed:
|
|
1095
|
+
print("modal not found; installing…")
|
|
1096
|
+
try:
|
|
1097
|
+
if shutil.which("uv"):
|
|
1098
|
+
code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
|
|
1099
|
+
else:
|
|
1100
|
+
code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
|
|
1101
|
+
if code != 0:
|
|
1102
|
+
print(out)
|
|
1103
|
+
print("Failed to install modal; continuing may fail.")
|
|
1104
|
+
return
|
|
1105
|
+
else:
|
|
1106
|
+
print("✓ modal installed successfully")
|
|
1107
|
+
modal_installed = True
|
|
1108
|
+
except Exception as exc:
|
|
1109
|
+
print(f"modal install error: {exc}")
|
|
1110
|
+
return
|
|
946
1111
|
|
|
947
|
-
|
|
948
|
-
|
|
1112
|
+
# Verify modal is importable
|
|
1113
|
+
if modal_installed:
|
|
1114
|
+
try:
|
|
1115
|
+
import importlib.util as _iu
|
|
949
1116
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1117
|
+
if _iu.find_spec("modal") is None:
|
|
1118
|
+
print("Warning: modal is still not importable after install attempt.")
|
|
1119
|
+
return
|
|
1120
|
+
except Exception:
|
|
1121
|
+
print("Warning: unable to verify modal installation.")
|
|
1122
|
+
return
|
|
1123
|
+
|
|
1124
|
+
# Check modal authentication status
|
|
1125
|
+
auth_ok, auth_msg = demo_core.modal_auth_status()
|
|
1126
|
+
if auth_ok:
|
|
1127
|
+
print(f"✓ Modal authenticated: {auth_msg}")
|
|
1128
|
+
else:
|
|
1129
|
+
print(f"\n⚠️ Modal authentication required")
|
|
1130
|
+
print(f" Status: {auth_msg}")
|
|
1131
|
+
print(f"\n To authenticate Modal, run:")
|
|
1132
|
+
print(f" modal setup")
|
|
1133
|
+
print(f"\n Or set environment variables:")
|
|
1134
|
+
print(f" export MODAL_TOKEN_ID=your-token-id")
|
|
1135
|
+
print(f" export MODAL_TOKEN_SECRET=your-token-secret")
|
|
1136
|
+
print(f"\n You can deploy later after authenticating.\n")
|
|
956
1137
|
|
|
957
1138
|
|
|
958
1139
|
def cmd_init(args: argparse.Namespace) -> int:
|
|
@@ -991,20 +1172,61 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
991
1172
|
assert selected is not None
|
|
992
1173
|
|
|
993
1174
|
default_subdir = selected.default_subdir or selected.template_id
|
|
994
|
-
|
|
1175
|
+
|
|
1176
|
+
# Check if default destination is already occupied and switch to local_demos/ if needed
|
|
1177
|
+
if args.dest:
|
|
1178
|
+
default_dest = Path(args.dest).expanduser().resolve()
|
|
1179
|
+
else:
|
|
1180
|
+
primary_dest = Path.cwd() / default_subdir
|
|
1181
|
+
if primary_dest.exists() and any(primary_dest.iterdir()):
|
|
1182
|
+
# Switch to local_demos/ automatically if primary location is occupied
|
|
1183
|
+
default_dest = (Path.cwd() / "local_demos" / default_subdir).resolve()
|
|
1184
|
+
else:
|
|
1185
|
+
default_dest = primary_dest.resolve()
|
|
1186
|
+
|
|
995
1187
|
try:
|
|
996
1188
|
dest_input = input(f"Destination directory [{default_dest}]: ").strip()
|
|
997
1189
|
except Exception:
|
|
998
1190
|
dest_input = ""
|
|
999
1191
|
destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
|
|
1000
1192
|
|
|
1193
|
+
# Track whether we should skip individual file prompts (if we already cleared the directory)
|
|
1194
|
+
directory_cleared = False
|
|
1195
|
+
|
|
1001
1196
|
if destination.exists():
|
|
1002
1197
|
if destination.is_file():
|
|
1003
1198
|
print(f"Destination {destination} is a file. Provide a directory path.")
|
|
1004
1199
|
return 1
|
|
1005
|
-
if
|
|
1006
|
-
|
|
1007
|
-
|
|
1200
|
+
if any(destination.iterdir()):
|
|
1201
|
+
try:
|
|
1202
|
+
response = (
|
|
1203
|
+
input(f"Destination {destination} is not empty. Overwrite? [y/N]: ")
|
|
1204
|
+
.strip()
|
|
1205
|
+
.lower()
|
|
1206
|
+
)
|
|
1207
|
+
except (EOFError, KeyboardInterrupt):
|
|
1208
|
+
print("\nCancelled.")
|
|
1209
|
+
return 1
|
|
1210
|
+
if response not in ("y", "yes"):
|
|
1211
|
+
print("Cancelled. Choose another directory or delete the existing one.")
|
|
1212
|
+
return 1
|
|
1213
|
+
# User agreed to overwrite - clear the entire directory including hidden files
|
|
1214
|
+
print(f"Clearing {destination}...")
|
|
1215
|
+
try:
|
|
1216
|
+
# Remove all contents including hidden files (.env, .git, etc.)
|
|
1217
|
+
shutil.rmtree(destination)
|
|
1218
|
+
except Exception as e:
|
|
1219
|
+
print(f"Error clearing directory: {e}")
|
|
1220
|
+
print("Please manually remove the directory and try again.")
|
|
1221
|
+
return 1
|
|
1222
|
+
# Recreate empty directory
|
|
1223
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
1224
|
+
# Verify it's actually empty
|
|
1225
|
+
if any(destination.iterdir()):
|
|
1226
|
+
print(f"Warning: Directory {destination} still contains files after clearing.")
|
|
1227
|
+
print("Some files may not have been removed. Please check manually.")
|
|
1228
|
+
return 1
|
|
1229
|
+
directory_cleared = True
|
|
1008
1230
|
else:
|
|
1009
1231
|
destination.mkdir(parents=True, exist_ok=True)
|
|
1010
1232
|
|
|
@@ -1018,29 +1240,83 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1018
1240
|
print(f"Template source missing: {src_path}")
|
|
1019
1241
|
return 1
|
|
1020
1242
|
dest_path = (destination / spec.destination).resolve()
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1243
|
+
|
|
1244
|
+
# Handle directory copying
|
|
1245
|
+
if src_path.is_dir():
|
|
1246
|
+
if dest_path.exists() and not directory_cleared:
|
|
1247
|
+
try:
|
|
1248
|
+
response = (
|
|
1249
|
+
input(f"Directory {dest_path.name} exists. Overwrite? [y/N]: ")
|
|
1250
|
+
.strip()
|
|
1251
|
+
.lower()
|
|
1252
|
+
)
|
|
1253
|
+
except (EOFError, KeyboardInterrupt):
|
|
1254
|
+
print("\nCancelled.")
|
|
1255
|
+
return 1
|
|
1256
|
+
if response not in ("y", "yes"):
|
|
1257
|
+
print(f"Skipping {dest_path.name}")
|
|
1258
|
+
continue
|
|
1259
|
+
shutil.rmtree(dest_path)
|
|
1260
|
+
elif dest_path.exists() and directory_cleared:
|
|
1261
|
+
shutil.rmtree(dest_path)
|
|
1262
|
+
shutil.copytree(src_path, dest_path)
|
|
1263
|
+
else:
|
|
1264
|
+
# Handle file copying
|
|
1265
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1266
|
+
if dest_path.exists() and not directory_cleared:
|
|
1267
|
+
try:
|
|
1268
|
+
response = (
|
|
1269
|
+
input(f"File {dest_path.name} exists. Overwrite? [y/N]: ")
|
|
1270
|
+
.strip()
|
|
1271
|
+
.lower()
|
|
1272
|
+
)
|
|
1273
|
+
except (EOFError, KeyboardInterrupt):
|
|
1274
|
+
print("\nCancelled.")
|
|
1275
|
+
return 1
|
|
1276
|
+
if response not in ("y", "yes"):
|
|
1277
|
+
print(f"Skipping {dest_path.name}")
|
|
1278
|
+
continue
|
|
1279
|
+
shutil.copy2(src_path, dest_path)
|
|
1280
|
+
if spec.make_executable:
|
|
1281
|
+
try:
|
|
1282
|
+
st = os.stat(dest_path)
|
|
1283
|
+
os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
1284
|
+
except Exception:
|
|
1285
|
+
pass
|
|
1032
1286
|
|
|
1033
1287
|
if selected.env_lines:
|
|
1034
1288
|
env_path = destination / ".env"
|
|
1035
|
-
|
|
1289
|
+
should_write = True
|
|
1290
|
+
if env_path.exists() and not directory_cleared:
|
|
1291
|
+
try:
|
|
1292
|
+
response = input(f"File .env exists. Overwrite? [y/N]: ").strip().lower()
|
|
1293
|
+
except (EOFError, KeyboardInterrupt):
|
|
1294
|
+
print("\nCancelled.")
|
|
1295
|
+
return 1
|
|
1296
|
+
should_write = response in ("y", "yes")
|
|
1297
|
+
if should_write:
|
|
1036
1298
|
_write_text(env_path, "\n".join(selected.env_lines) + "\n")
|
|
1299
|
+
elif not directory_cleared:
|
|
1300
|
+
print("Skipping .env")
|
|
1037
1301
|
|
|
1038
1302
|
config_src = selected.config_source_path()
|
|
1039
1303
|
if config_src and config_src.exists():
|
|
1040
1304
|
cfg_dst = (destination / selected.config_destination).resolve()
|
|
1041
|
-
|
|
1305
|
+
should_copy = True
|
|
1306
|
+
if cfg_dst.exists() and not directory_cleared:
|
|
1307
|
+
try:
|
|
1308
|
+
response = (
|
|
1309
|
+
input(f"File {cfg_dst.name} exists. Overwrite? [y/N]: ").strip().lower()
|
|
1310
|
+
)
|
|
1311
|
+
except (EOFError, KeyboardInterrupt):
|
|
1312
|
+
print("\nCancelled.")
|
|
1313
|
+
return 1
|
|
1314
|
+
should_copy = response in ("y", "yes")
|
|
1315
|
+
if should_copy:
|
|
1042
1316
|
cfg_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
1043
1317
|
shutil.copy2(config_src, cfg_dst)
|
|
1318
|
+
elif not directory_cleared:
|
|
1319
|
+
print(f"Skipping {cfg_dst.name}")
|
|
1044
1320
|
|
|
1045
1321
|
if selected.post_copy is not None:
|
|
1046
1322
|
try:
|
|
@@ -1049,6 +1325,14 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1049
1325
|
print(f"Post-processing failed: {post_exc}")
|
|
1050
1326
|
return 1
|
|
1051
1327
|
|
|
1328
|
+
# Store demo directory for subsequent commands
|
|
1329
|
+
demo_core.persist_demo_dir(str(destination))
|
|
1330
|
+
|
|
1331
|
+
# Store .env path if it was created
|
|
1332
|
+
env_file = destination / ".env"
|
|
1333
|
+
if env_file.exists():
|
|
1334
|
+
demo_core.persist_env_file_path(str(env_file))
|
|
1335
|
+
|
|
1052
1336
|
print(f"Demo template '{selected.name}' materialised at {destination}.")
|
|
1053
1337
|
print("Files created:")
|
|
1054
1338
|
for spec in selected.iter_copy_specs():
|
|
@@ -1057,6 +1341,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1057
1341
|
print(" - .env")
|
|
1058
1342
|
if selected.config_source_path():
|
|
1059
1343
|
print(f" - {selected.config_destination}")
|
|
1344
|
+
print("\nDemo directory stored. Subsequent commands will use this directory automatically.")
|
|
1060
1345
|
print("Review the files, edit .env, and run any provided deploy scripts when ready.")
|
|
1061
1346
|
return 0
|
|
1062
1347
|
except KeyboardInterrupt:
|
|
@@ -1067,8 +1352,11 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1067
1352
|
return 1
|
|
1068
1353
|
|
|
1069
1354
|
|
|
1070
|
-
def _http(
|
|
1355
|
+
def _http(
|
|
1356
|
+
method: str, url: str, headers: Dict[str, str] | None = None, body: Dict[str, Any] | None = None
|
|
1357
|
+
) -> tuple[int, Dict[str, Any] | str]:
|
|
1071
1358
|
import urllib.request, urllib.error, json as _json, ssl
|
|
1359
|
+
|
|
1072
1360
|
data = None
|
|
1073
1361
|
if body is not None:
|
|
1074
1362
|
data = _json.dumps(body).encode("utf-8")
|
|
@@ -1106,6 +1394,12 @@ def _write_text(path: str, content: str) -> None:
|
|
|
1106
1394
|
|
|
1107
1395
|
|
|
1108
1396
|
def cmd_run(args: argparse.Namespace) -> int:
|
|
1397
|
+
# Change to demo directory if stored
|
|
1398
|
+
demo_dir = demo_core.load_demo_dir()
|
|
1399
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
1400
|
+
os.chdir(demo_dir)
|
|
1401
|
+
print(f"Using demo directory: {demo_dir}")
|
|
1402
|
+
|
|
1109
1403
|
env = demo_core.load_env()
|
|
1110
1404
|
cwd_env_path = os.path.join(os.getcwd(), ".env")
|
|
1111
1405
|
local_env = demo_core.load_dotenv_file(cwd_env_path)
|
|
@@ -1148,7 +1442,11 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1148
1442
|
# Detect monorepo launcher and delegate if available (aligns with run_clustered.sh which works)
|
|
1149
1443
|
launcher = "/Users/joshpurtell/Documents/GitHub/monorepo/tests/applications/math/rl/start_math_clustered.py"
|
|
1150
1444
|
if os.path.isfile(launcher):
|
|
1151
|
-
backend_base =
|
|
1445
|
+
backend_base = (
|
|
1446
|
+
env.dev_backend_url[:-4]
|
|
1447
|
+
if env.dev_backend_url.endswith("/api")
|
|
1448
|
+
else env.dev_backend_url
|
|
1449
|
+
)
|
|
1152
1450
|
run_env = os.environ.copy()
|
|
1153
1451
|
run_env["BACKEND_URL"] = backend_base
|
|
1154
1452
|
run_env["SYNTH_API_KEY"] = env.synth_api_key
|
|
@@ -1181,7 +1479,9 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1181
1479
|
print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
|
|
1182
1480
|
if ek:
|
|
1183
1481
|
print(f" {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
|
|
1184
|
-
print(
|
|
1482
|
+
print(
|
|
1483
|
+
"Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported."
|
|
1484
|
+
)
|
|
1185
1485
|
return code
|
|
1186
1486
|
|
|
1187
1487
|
# Fallback: legacy jobs API flow
|
|
@@ -1222,7 +1522,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1222
1522
|
if inline_cfg["compute"].get("gpu_type"):
|
|
1223
1523
|
compute["gpu_type"] = str(inline_cfg["compute"]["gpu_type"]).upper()
|
|
1224
1524
|
if inline_cfg["compute"].get("gpu_count"):
|
|
1225
|
-
compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
|
|
1525
|
+
compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
|
|
1226
1526
|
if not compute:
|
|
1227
1527
|
topo = inline_cfg.get("topology") or {}
|
|
1228
1528
|
gshape = str(topo.get("gpu_type") or "")
|
|
@@ -1235,10 +1535,15 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1235
1535
|
}
|
|
1236
1536
|
if compute:
|
|
1237
1537
|
body["compute"] = compute
|
|
1238
|
-
code, js = _http(
|
|
1239
|
-
"
|
|
1240
|
-
|
|
1241
|
-
|
|
1538
|
+
code, js = _http(
|
|
1539
|
+
"POST",
|
|
1540
|
+
api + "/rl/jobs",
|
|
1541
|
+
headers={
|
|
1542
|
+
"Content-Type": "application/json",
|
|
1543
|
+
"Authorization": f"Bearer {env.synth_api_key}",
|
|
1544
|
+
},
|
|
1545
|
+
body=body,
|
|
1546
|
+
)
|
|
1242
1547
|
if code not in (200, 201) or not isinstance(js, dict):
|
|
1243
1548
|
print("Job create failed:", code)
|
|
1244
1549
|
print(f"Backend: {api}")
|
|
@@ -1276,7 +1581,9 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1276
1581
|
try:
|
|
1277
1582
|
sent_key = detail.get("sent_key")
|
|
1278
1583
|
if isinstance(sent_key, str):
|
|
1279
|
-
print(
|
|
1584
|
+
print(
|
|
1585
|
+
f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}"
|
|
1586
|
+
)
|
|
1280
1587
|
except Exception:
|
|
1281
1588
|
pass
|
|
1282
1589
|
try:
|
|
@@ -1306,12 +1613,19 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1306
1613
|
# Extra hints for auth failures
|
|
1307
1614
|
try:
|
|
1308
1615
|
sk = (env.synth_api_key or "").strip()
|
|
1309
|
-
if int(code) == 401 or (
|
|
1616
|
+
if int(code) == 401 or (
|
|
1617
|
+
isinstance(js, dict)
|
|
1618
|
+
and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())
|
|
1619
|
+
):
|
|
1310
1620
|
base_url = env.dev_backend_url
|
|
1311
|
-
print(
|
|
1621
|
+
print(
|
|
1622
|
+
"Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url
|
|
1623
|
+
)
|
|
1312
1624
|
if sk:
|
|
1313
1625
|
print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
|
|
1314
|
-
print(
|
|
1626
|
+
print(
|
|
1627
|
+
"Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid."
|
|
1628
|
+
)
|
|
1315
1629
|
except Exception:
|
|
1316
1630
|
pass
|
|
1317
1631
|
return 2
|
|
@@ -1363,9 +1677,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1363
1677
|
"rl.performance.metrics",
|
|
1364
1678
|
):
|
|
1365
1679
|
print(f"[{seq}] {typ}: {msg}")
|
|
1366
|
-
mc, mj = _http(
|
|
1367
|
-
"GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50"
|
|
1368
|
-
)
|
|
1680
|
+
mc, mj = _http("GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50")
|
|
1369
1681
|
if mc == 200 and isinstance(mj, dict):
|
|
1370
1682
|
pts = mj.get("points") or []
|
|
1371
1683
|
for p in pts:
|
|
@@ -1384,17 +1696,23 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1384
1696
|
p = argparse.ArgumentParser(prog="synth-ai")
|
|
1385
1697
|
sub = p.add_subparsers(dest="cmd")
|
|
1386
1698
|
|
|
1387
|
-
def _add_parser(
|
|
1699
|
+
def _add_parser(
|
|
1700
|
+
names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]
|
|
1701
|
+
) -> None:
|
|
1388
1702
|
for name in names:
|
|
1389
1703
|
parser = sub.add_parser(name)
|
|
1390
1704
|
configure(parser)
|
|
1391
1705
|
|
|
1392
|
-
_add_parser(
|
|
1706
|
+
_add_parser(
|
|
1707
|
+
["rl_demo.setup", "demo.setup"],
|
|
1708
|
+
configure=lambda parser: parser.set_defaults(func=cmd_setup),
|
|
1709
|
+
)
|
|
1393
1710
|
|
|
1394
1711
|
def _init_opts(parser):
|
|
1395
1712
|
parser.add_argument("--template", type=str, default=None, help="Template id to instantiate")
|
|
1396
|
-
parser.add_argument(
|
|
1397
|
-
|
|
1713
|
+
parser.add_argument(
|
|
1714
|
+
"--dest", type=str, default=None, help="Destination directory for files"
|
|
1715
|
+
)
|
|
1398
1716
|
parser.set_defaults(func=cmd_init)
|
|
1399
1717
|
|
|
1400
1718
|
_add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
|
|
@@ -1402,18 +1720,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1402
1720
|
# (prepare command removed)
|
|
1403
1721
|
|
|
1404
1722
|
def _deploy_opts(parser):
|
|
1405
|
-
parser.add_argument(
|
|
1406
|
-
|
|
1723
|
+
parser.add_argument(
|
|
1724
|
+
"--local", action="store_true", help="Run local FastAPI instead of Modal deploy"
|
|
1725
|
+
)
|
|
1726
|
+
parser.add_argument(
|
|
1727
|
+
"--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy"
|
|
1728
|
+
)
|
|
1407
1729
|
parser.add_argument("--name", type=str, default=None, help="Modal app name")
|
|
1408
|
-
parser.add_argument(
|
|
1730
|
+
parser.add_argument(
|
|
1731
|
+
"--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)"
|
|
1732
|
+
)
|
|
1409
1733
|
parser.set_defaults(func=cmd_deploy)
|
|
1410
1734
|
|
|
1411
1735
|
_add_parser(["rl_demo.deploy", "demo.deploy"], configure=_deploy_opts)
|
|
1412
1736
|
|
|
1413
|
-
_add_parser(
|
|
1737
|
+
_add_parser(
|
|
1738
|
+
["rl_demo.configure", "demo.configure"],
|
|
1739
|
+
configure=lambda parser: parser.set_defaults(func=cmd_run),
|
|
1740
|
+
)
|
|
1414
1741
|
|
|
1415
1742
|
def _run_opts(parser):
|
|
1416
|
-
parser.add_argument(
|
|
1743
|
+
parser.add_argument(
|
|
1744
|
+
"--config", type=str, default=None, help="Path to TOML config (skip prompt)"
|
|
1745
|
+
)
|
|
1417
1746
|
parser.add_argument("--batch-size", type=int, default=None)
|
|
1418
1747
|
parser.add_argument("--group-size", type=int, default=None)
|
|
1419
1748
|
parser.add_argument("--model", type=str, default=None)
|