synth-ai 0.2.8.dev13__py3-none-any.whl → 0.2.9.dev1__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.
- synth_ai/api/train/cli.py +21 -0
- synth_ai/api/train/config_finder.py +54 -6
- synth_ai/api/train/task_app.py +70 -5
- synth_ai/cli/rl_demo.py +16 -4
- synth_ai/cli/root.py +36 -5
- synth_ai/cli/task_apps.py +792 -205
- synth_ai/demo_registry.py +258 -0
- synth_ai/demos/core/cli.py +147 -111
- synth_ai/demos/demo_task_apps/__init__.py +7 -1
- synth_ai/demos/demo_task_apps/math/config.toml +55 -110
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +157 -21
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +39 -0
- synth_ai/task/auth.py +33 -12
- synth_ai/task/client.py +20 -3
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/METADATA +1 -1
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/RECORD +20 -18
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.8.dev13.dist-info → synth_ai-0.2.9.dev1.dist-info}/top_level.txt +0 -0
synth_ai/api/train/cli.py
CHANGED
|
@@ -161,6 +161,27 @@ def train_command(
|
|
|
161
161
|
explicit_env_paths=env_files,
|
|
162
162
|
required_keys=required_keys,
|
|
163
163
|
)
|
|
164
|
+
|
|
165
|
+
missing_keys = [
|
|
166
|
+
spec.name
|
|
167
|
+
for spec in required_keys
|
|
168
|
+
if not spec.optional and not (env_values.get(spec.name) or os.environ.get(spec.name))
|
|
169
|
+
]
|
|
170
|
+
if missing_keys:
|
|
171
|
+
try:
|
|
172
|
+
from synth_ai.cli.task_apps import _interactive_fill_env
|
|
173
|
+
except Exception as exc: # pragma: no cover - protective fallback
|
|
174
|
+
raise click.ClickException(f"Unable to prompt for env values: {exc}") from exc
|
|
175
|
+
|
|
176
|
+
target_dir = cfg_path.parent
|
|
177
|
+
generated = _interactive_fill_env(target_dir / ".env")
|
|
178
|
+
if generated is None:
|
|
179
|
+
raise click.ClickException("Required environment values missing; aborting.")
|
|
180
|
+
env_path, env_values = resolve_env(
|
|
181
|
+
config_path=cfg_path,
|
|
182
|
+
explicit_env_paths=(str(generated),),
|
|
183
|
+
required_keys=required_keys,
|
|
184
|
+
)
|
|
164
185
|
click.echo(f"Using env file: {env_path}")
|
|
165
186
|
|
|
166
187
|
synth_key = env_values.get("SYNTH_API_KEY") or os.environ.get("SYNTH_API_KEY")
|
|
@@ -37,19 +37,67 @@ def _iter_candidate_paths() -> Iterable[Path]:
|
|
|
37
37
|
seen.add(resolved)
|
|
38
38
|
yield resolved
|
|
39
39
|
|
|
40
|
+
# Additionally, discover configs anywhere under the current working directory
|
|
41
|
+
# so users can run `uvx synth-ai train` from project roots without passing --config.
|
|
42
|
+
try:
|
|
43
|
+
cwd = Path.cwd().resolve()
|
|
44
|
+
except Exception:
|
|
45
|
+
cwd = None
|
|
46
|
+
if cwd and cwd.exists():
|
|
47
|
+
for path in cwd.rglob("*.toml"):
|
|
48
|
+
if any(part in _SKIP_DIRS for part in path.parts):
|
|
49
|
+
continue
|
|
50
|
+
resolved = path.resolve()
|
|
51
|
+
if resolved in seen:
|
|
52
|
+
continue
|
|
53
|
+
seen.add(resolved)
|
|
54
|
+
yield resolved
|
|
55
|
+
|
|
40
56
|
|
|
41
57
|
def _infer_config_type(data: dict) -> str:
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
# 1) Strong signals from [algorithm]
|
|
59
|
+
algo = data.get("algorithm")
|
|
60
|
+
if isinstance(algo, dict):
|
|
61
|
+
method = str(algo.get("method") or "").lower()
|
|
62
|
+
algo_type = str(algo.get("type") or "").lower()
|
|
63
|
+
variety = str(algo.get("variety") or "").lower()
|
|
64
|
+
|
|
65
|
+
# RL indicators
|
|
66
|
+
if method in {"policy_gradient", "ppo", "reinforce"}:
|
|
67
|
+
return "rl"
|
|
68
|
+
if algo_type == "online":
|
|
69
|
+
return "rl"
|
|
70
|
+
if variety in {"gspo", "grpo", "ppo"}:
|
|
71
|
+
return "rl"
|
|
72
|
+
|
|
73
|
+
# SFT indicators
|
|
74
|
+
if method in {"supervised_finetune", "sft"}:
|
|
75
|
+
return "sft"
|
|
76
|
+
if algo_type == "offline":
|
|
77
|
+
return "sft"
|
|
78
|
+
if variety in {"fft"}:
|
|
79
|
+
return "sft"
|
|
80
|
+
|
|
81
|
+
# 2) Other RL signals
|
|
44
82
|
if data.get("job_type") == "rl":
|
|
45
83
|
return "rl"
|
|
46
84
|
services = data.get("services")
|
|
47
85
|
if isinstance(services, dict) and ("task_url" in services or "environment" in services):
|
|
48
86
|
return "rl"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
|
|
88
|
+
# 3) Other SFT signals
|
|
89
|
+
training = data.get("training")
|
|
90
|
+
if isinstance(training, dict):
|
|
91
|
+
mode = str(training.get("mode") or "").lower()
|
|
92
|
+
if mode.startswith("sft") or mode == "sft_offline":
|
|
93
|
+
return "sft"
|
|
94
|
+
hyper = data.get("hyperparameters")
|
|
95
|
+
if isinstance(hyper, dict):
|
|
96
|
+
kind = str(hyper.get("train_kind") or "").lower()
|
|
97
|
+
if kind in {"sft", "fft"}:
|
|
98
|
+
return "sft"
|
|
99
|
+
|
|
100
|
+
# 4) Fallback
|
|
53
101
|
return "unknown"
|
|
54
102
|
|
|
55
103
|
|
synth_ai/api/train/task_app.py
CHANGED
|
@@ -18,23 +18,88 @@ class TaskAppHealth:
|
|
|
18
18
|
detail: str | None = None
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _health_response_ok(resp: requests.Response | None) -> tuple[bool, str]:
|
|
22
|
+
if resp is None:
|
|
23
|
+
return False, ""
|
|
24
|
+
status = resp.status_code
|
|
25
|
+
if status == 200:
|
|
26
|
+
return True, ""
|
|
27
|
+
if status in {401, 403}:
|
|
28
|
+
try:
|
|
29
|
+
payload = resp.json()
|
|
30
|
+
except ValueError:
|
|
31
|
+
payload = {}
|
|
32
|
+
prefix = payload.get("expected_api_key_prefix")
|
|
33
|
+
detail = str(payload.get("detail", ""))
|
|
34
|
+
if prefix or "expected prefix" in detail.lower():
|
|
35
|
+
note = "auth-optional"
|
|
36
|
+
if prefix:
|
|
37
|
+
note += f" (expected-prefix={prefix})"
|
|
38
|
+
return True, note
|
|
39
|
+
return False, ""
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
def check_task_app_health(base_url: str, api_key: str, *, timeout: float = 10.0) -> TaskAppHealth:
|
|
43
|
+
# Send ALL known environment keys so the server can authorize any valid one
|
|
44
|
+
import os
|
|
22
45
|
headers = {"X-API-Key": api_key}
|
|
46
|
+
aliases = (os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
|
|
47
|
+
keys: list[str] = [api_key]
|
|
48
|
+
if aliases:
|
|
49
|
+
keys.extend([p.strip() for p in aliases.split(",") if p.strip()])
|
|
50
|
+
if keys:
|
|
51
|
+
headers["X-API-Keys"] = ",".join(keys)
|
|
52
|
+
headers.setdefault("Authorization", f"Bearer {api_key}")
|
|
23
53
|
base = base_url.rstrip("/")
|
|
24
|
-
health_resp = None
|
|
25
54
|
detail_parts: list[str] = []
|
|
55
|
+
|
|
56
|
+
health_resp: requests.Response | None = None
|
|
57
|
+
health_ok = False
|
|
26
58
|
try:
|
|
27
59
|
health_resp = http_get(f"{base}/health", headers=headers, timeout=timeout)
|
|
28
|
-
|
|
60
|
+
health_ok, note = _health_response_ok(health_resp)
|
|
61
|
+
suffix = f" ({note})" if note else ""
|
|
62
|
+
# On non-200, include brief JSON detail if present
|
|
63
|
+
if not health_ok and health_resp is not None:
|
|
64
|
+
try:
|
|
65
|
+
hjs = health_resp.json()
|
|
66
|
+
# pull a few helpful fields without dumping everything
|
|
67
|
+
expected = hjs.get("expected_api_key_prefix")
|
|
68
|
+
authorized = hjs.get("authorized")
|
|
69
|
+
detail = hjs.get("detail")
|
|
70
|
+
extras = []
|
|
71
|
+
if authorized is not None:
|
|
72
|
+
extras.append(f"authorized={authorized}")
|
|
73
|
+
if expected:
|
|
74
|
+
extras.append(f"expected_prefix={expected}")
|
|
75
|
+
if detail:
|
|
76
|
+
extras.append(f"detail={str(detail)[:80]}")
|
|
77
|
+
if extras:
|
|
78
|
+
suffix += " [" + ", ".join(extras) + "]"
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
detail_parts.append(f"/health={health_resp.status_code}{suffix}")
|
|
29
82
|
except requests.RequestException as exc:
|
|
30
83
|
detail_parts.append(f"/health_error={exc}")
|
|
31
|
-
|
|
84
|
+
|
|
85
|
+
task_resp: requests.Response | None = None
|
|
86
|
+
task_ok = False
|
|
32
87
|
try:
|
|
33
88
|
task_resp = http_get(f"{base}/task_info", headers=headers, timeout=timeout)
|
|
34
|
-
|
|
89
|
+
task_ok = bool(task_resp.status_code == 200)
|
|
90
|
+
if not task_ok and task_resp is not None:
|
|
91
|
+
try:
|
|
92
|
+
tjs = task_resp.json()
|
|
93
|
+
msg = tjs.get("detail") or tjs.get("status")
|
|
94
|
+
detail_parts.append(f"/task_info={task_resp.status_code} ({str(msg)[:80]})")
|
|
95
|
+
except Exception:
|
|
96
|
+
detail_parts.append(f"/task_info={task_resp.status_code}")
|
|
97
|
+
else:
|
|
98
|
+
detail_parts.append(f"/task_info={task_resp.status_code}")
|
|
35
99
|
except requests.RequestException as exc:
|
|
36
100
|
detail_parts.append(f"/task_info_error={exc}")
|
|
37
|
-
|
|
101
|
+
|
|
102
|
+
ok = bool(health_ok and task_ok)
|
|
38
103
|
detail = ", ".join(detail_parts)
|
|
39
104
|
return TaskAppHealth(
|
|
40
105
|
ok=ok,
|
synth_ai/cli/rl_demo.py
CHANGED
|
@@ -67,9 +67,15 @@ def register(cli):
|
|
|
67
67
|
_forward(["rl_demo.configure"])
|
|
68
68
|
|
|
69
69
|
@_rlg.command("init")
|
|
70
|
-
@click.option("--
|
|
71
|
-
|
|
70
|
+
@click.option("--template", type=str, default=None, help="Template id to instantiate")
|
|
71
|
+
@click.option("--dest", type=click.Path(), default=None, help="Destination directory for files")
|
|
72
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files in destination")
|
|
73
|
+
def rl_init(template: str | None, dest: str | None, force: bool):
|
|
72
74
|
args = ["rl_demo.init"]
|
|
75
|
+
if template:
|
|
76
|
+
args.extend(["--template", template])
|
|
77
|
+
if dest:
|
|
78
|
+
args.extend(["--dest", dest])
|
|
73
79
|
if force:
|
|
74
80
|
args.append("--force")
|
|
75
81
|
_forward(args)
|
|
@@ -130,9 +136,15 @@ def register(cli):
|
|
|
130
136
|
_forward(["rl_demo.configure"])
|
|
131
137
|
|
|
132
138
|
@cli.command("rl_demo.init")
|
|
133
|
-
@click.option("--
|
|
134
|
-
|
|
139
|
+
@click.option("--template", type=str, default=None, help="Template id to instantiate")
|
|
140
|
+
@click.option("--dest", type=click.Path(), default=None, help="Destination directory for files")
|
|
141
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files in destination")
|
|
142
|
+
def rl_init_alias(template: str | None, dest: str | None, force: bool):
|
|
135
143
|
args = ["rl_demo.init"]
|
|
144
|
+
if template:
|
|
145
|
+
args.extend(["--template", template])
|
|
146
|
+
if dest:
|
|
147
|
+
args.extend(["--dest", dest])
|
|
136
148
|
if force:
|
|
137
149
|
args.append("--force")
|
|
138
150
|
_forward(args)
|
synth_ai/cli/root.py
CHANGED
|
@@ -14,6 +14,20 @@ import sys
|
|
|
14
14
|
import time
|
|
15
15
|
|
|
16
16
|
import click
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
19
|
+
try:
|
|
20
|
+
__pkg_version__ = _pkg_version("synth-ai")
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
try:
|
|
23
|
+
from synth_ai import __version__ as __pkg_version__ # type: ignore
|
|
24
|
+
except Exception:
|
|
25
|
+
__pkg_version__ = "unknown"
|
|
26
|
+
except Exception:
|
|
27
|
+
try:
|
|
28
|
+
from synth_ai import __version__ as __pkg_version__ # type: ignore
|
|
29
|
+
except Exception:
|
|
30
|
+
__pkg_version__ = "unknown"
|
|
17
31
|
|
|
18
32
|
|
|
19
33
|
def find_sqld_binary() -> str | None:
|
|
@@ -66,9 +80,10 @@ rm -rf "$TMP_DIR"
|
|
|
66
80
|
return os.path.expanduser("~/.local/bin/sqld")
|
|
67
81
|
|
|
68
82
|
|
|
69
|
-
@click.group()
|
|
83
|
+
@click.group(help=f"Synth AI v{__pkg_version__} - Software for aiding the best and multiplying the will.")
|
|
84
|
+
@click.version_option(version=__pkg_version__, prog_name="synth-ai")
|
|
70
85
|
def cli():
|
|
71
|
-
"""
|
|
86
|
+
"""Top-level command group for Synth AI."""
|
|
72
87
|
|
|
73
88
|
|
|
74
89
|
# === Legacy demo command group (aliases new rl_demo implementation) ===
|
|
@@ -84,7 +99,7 @@ def _forward_to_demo(args: list[str]) -> None:
|
|
|
84
99
|
except Exception as e: # pragma: no cover
|
|
85
100
|
click.echo(f"Failed to import demo CLI: {e}")
|
|
86
101
|
sys.exit(1)
|
|
87
|
-
rc = int(demo_cli
|
|
102
|
+
rc = int(getattr(demo_cli, "main")(args) or 0) # type: ignore[attr-defined]
|
|
88
103
|
if rc != 0:
|
|
89
104
|
sys.exit(rc)
|
|
90
105
|
|
|
@@ -123,6 +138,22 @@ def setup():
|
|
|
123
138
|
_forward_to_demo(["rl_demo.setup"])
|
|
124
139
|
|
|
125
140
|
|
|
141
|
+
@demo.command()
|
|
142
|
+
@click.option("--template", type=str, default=None, help="Template id to instantiate")
|
|
143
|
+
@click.option("--dest", type=str, default=None, help="Destination directory for files")
|
|
144
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files in destination")
|
|
145
|
+
def init(template: str | None, dest: str | None, force: bool):
|
|
146
|
+
"""Copy demo task app template into the current directory."""
|
|
147
|
+
args: list[str] = ["demo.init"]
|
|
148
|
+
if template:
|
|
149
|
+
args.extend(["--template", template])
|
|
150
|
+
if dest:
|
|
151
|
+
args.extend(["--dest", dest])
|
|
152
|
+
if force:
|
|
153
|
+
args.append("--force")
|
|
154
|
+
_forward_to_demo(args)
|
|
155
|
+
|
|
156
|
+
|
|
126
157
|
@demo.command()
|
|
127
158
|
@click.option("--batch-size", type=int, default=None)
|
|
128
159
|
@click.option("--group-size", type=int, default=None)
|
|
@@ -142,8 +173,8 @@ def run(batch_size: int | None, group_size: int | None, model: str | None, timeo
|
|
|
142
173
|
_forward_to_demo(args)
|
|
143
174
|
|
|
144
175
|
|
|
145
|
-
@cli.command()
|
|
146
|
-
def
|
|
176
|
+
@cli.command(name="setup")
|
|
177
|
+
def setup_command():
|
|
147
178
|
"""Perform SDK handshake and write keys to .env."""
|
|
148
179
|
_forward_to_demo(["rl_demo.setup"])
|
|
149
180
|
|