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.

Files changed (157) hide show
  1. examples/common_old/backend.py +0 -1
  2. examples/crafter_debug_render.py +15 -6
  3. examples/evals_old/compare_models.py +1 -0
  4. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +6 -2
  5. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +4 -4
  6. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +4 -3
  7. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +6 -2
  8. examples/finetuning_old/synth_qwen_v1/finetune.py +1 -1
  9. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +4 -4
  10. examples/finetuning_old/synth_qwen_v1/infer.py +1 -2
  11. examples/finetuning_old/synth_qwen_v1/poll.py +4 -2
  12. examples/finetuning_old/synth_qwen_v1/prepare_data.py +8 -8
  13. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +5 -4
  14. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +11 -8
  15. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +17 -12
  16. examples/finetuning_old/synth_qwen_v1/upload_data.py +1 -1
  17. examples/finetuning_old/synth_qwen_v1/util.py +7 -2
  18. examples/rl/configs/eval_base_qwen.toml +1 -1
  19. examples/rl/configs/rl_from_base_qwen17.toml +1 -1
  20. examples/rl/download_dataset.py +26 -10
  21. examples/rl/run_eval.py +17 -15
  22. examples/rl/run_rl_and_save.py +24 -7
  23. examples/rl/task_app/math_single_step.py +128 -11
  24. examples/rl/task_app/math_task_app.py +11 -3
  25. examples/rl_old/task_app.py +222 -53
  26. examples/warming_up_to_rl/analyze_trace_db.py +7 -5
  27. examples/warming_up_to_rl/export_trace_sft.py +141 -16
  28. examples/warming_up_to_rl/groq_test.py +11 -4
  29. examples/warming_up_to_rl/manage_secrets.py +15 -6
  30. examples/warming_up_to_rl/readme.md +9 -2
  31. examples/warming_up_to_rl/run_eval.py +108 -30
  32. examples/warming_up_to_rl/run_fft_and_save.py +128 -52
  33. examples/warming_up_to_rl/run_local_rollout.py +87 -36
  34. examples/warming_up_to_rl/run_local_rollout_modal.py +113 -25
  35. examples/warming_up_to_rl/run_local_rollout_parallel.py +80 -16
  36. examples/warming_up_to_rl/run_local_rollout_traced.py +125 -20
  37. examples/warming_up_to_rl/run_rl_and_save.py +31 -7
  38. examples/warming_up_to_rl/run_rollout_remote.py +37 -10
  39. examples/warming_up_to_rl/task_app/grpo_crafter.py +90 -27
  40. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +9 -27
  41. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +46 -108
  42. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
  43. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
  44. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
  45. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +50 -17
  46. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +35 -21
  47. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +8 -4
  48. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +29 -26
  49. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
  50. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +17 -13
  51. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
  52. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +106 -63
  53. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +82 -84
  54. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +76 -59
  55. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
  56. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +43 -49
  57. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +5 -15
  58. synth_ai/__init__.py +1 -0
  59. synth_ai/api/train/builders.py +34 -10
  60. synth_ai/api/train/cli.py +172 -32
  61. synth_ai/api/train/config_finder.py +59 -4
  62. synth_ai/api/train/env_resolver.py +32 -14
  63. synth_ai/api/train/pollers.py +11 -3
  64. synth_ai/api/train/task_app.py +4 -1
  65. synth_ai/api/train/utils.py +20 -4
  66. synth_ai/cli/__init__.py +11 -4
  67. synth_ai/cli/balance.py +1 -1
  68. synth_ai/cli/demo.py +19 -5
  69. synth_ai/cli/rl_demo.py +75 -16
  70. synth_ai/cli/root.py +116 -37
  71. synth_ai/cli/task_apps.py +1286 -170
  72. synth_ai/cli/traces.py +1 -0
  73. synth_ai/cli/turso.py +73 -0
  74. synth_ai/core/experiment.py +0 -2
  75. synth_ai/demo_registry.py +67 -30
  76. synth_ai/demos/core/cli.py +493 -164
  77. synth_ai/demos/demo_task_apps/core.py +50 -6
  78. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
  79. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +36 -28
  80. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  81. synth_ai/demos/demo_task_apps/math/deploy_modal.py +0 -2
  82. synth_ai/demos/demo_task_apps/math/modal_task_app.py +168 -65
  83. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
  84. synth_ai/environments/examples/bandit/engine.py +12 -4
  85. synth_ai/environments/examples/bandit/taskset.py +4 -4
  86. synth_ai/environments/reproducibility/tree.py +3 -1
  87. synth_ai/environments/service/core_routes.py +6 -2
  88. synth_ai/evals/base.py +0 -2
  89. synth_ai/experimental/synth_oss.py +11 -12
  90. synth_ai/handshake.py +3 -1
  91. synth_ai/http_client.py +31 -7
  92. synth_ai/inference/__init__.py +0 -2
  93. synth_ai/inference/client.py +8 -4
  94. synth_ai/jobs/client.py +40 -10
  95. synth_ai/learning/client.py +33 -8
  96. synth_ai/learning/config.py +0 -2
  97. synth_ai/learning/constants.py +0 -2
  98. synth_ai/learning/ft_client.py +6 -3
  99. synth_ai/learning/health.py +9 -2
  100. synth_ai/learning/jobs.py +17 -5
  101. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +1 -3
  102. synth_ai/learning/prompts/random_search.py +4 -1
  103. synth_ai/learning/prompts/run_random_search_banking77.py +6 -1
  104. synth_ai/learning/rl_client.py +42 -14
  105. synth_ai/learning/sse.py +0 -2
  106. synth_ai/learning/validators.py +6 -2
  107. synth_ai/lm/caching/ephemeral.py +1 -3
  108. synth_ai/lm/core/exceptions.py +0 -2
  109. synth_ai/lm/core/main.py +13 -1
  110. synth_ai/lm/core/synth_models.py +0 -1
  111. synth_ai/lm/core/vendor_clients.py +4 -2
  112. synth_ai/lm/overrides.py +2 -2
  113. synth_ai/lm/vendors/core/anthropic_api.py +7 -7
  114. synth_ai/lm/vendors/core/openai_api.py +2 -0
  115. synth_ai/lm/vendors/openai_standard.py +3 -1
  116. synth_ai/lm/vendors/openai_standard_responses.py +6 -3
  117. synth_ai/lm/vendors/supported/custom_endpoint.py +1 -3
  118. synth_ai/lm/vendors/synth_client.py +37 -10
  119. synth_ai/rl/__init__.py +0 -1
  120. synth_ai/rl/contracts.py +0 -2
  121. synth_ai/rl/env_keys.py +6 -1
  122. synth_ai/task/__init__.py +1 -0
  123. synth_ai/task/apps/__init__.py +11 -11
  124. synth_ai/task/auth.py +29 -17
  125. synth_ai/task/client.py +3 -1
  126. synth_ai/task/contracts.py +1 -0
  127. synth_ai/task/datasets.py +3 -1
  128. synth_ai/task/errors.py +3 -2
  129. synth_ai/task/health.py +0 -2
  130. synth_ai/task/json.py +0 -1
  131. synth_ai/task/proxy.py +2 -5
  132. synth_ai/task/rubrics.py +9 -3
  133. synth_ai/task/server.py +31 -5
  134. synth_ai/task/tracing_utils.py +8 -3
  135. synth_ai/task/validators.py +0 -1
  136. synth_ai/task/vendors.py +0 -1
  137. synth_ai/tracing_v3/db_config.py +26 -1
  138. synth_ai/tracing_v3/decorators.py +1 -0
  139. synth_ai/tracing_v3/examples/basic_usage.py +3 -2
  140. synth_ai/tracing_v3/hooks.py +2 -0
  141. synth_ai/tracing_v3/replica_sync.py +1 -0
  142. synth_ai/tracing_v3/session_tracer.py +24 -3
  143. synth_ai/tracing_v3/storage/base.py +4 -1
  144. synth_ai/tracing_v3/storage/factory.py +0 -1
  145. synth_ai/tracing_v3/turso/manager.py +102 -38
  146. synth_ai/tracing_v3/turso/models.py +4 -1
  147. synth_ai/tracing_v3/utils.py +1 -0
  148. synth_ai/v0/tracing/upload.py +32 -135
  149. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/METADATA +1 -1
  150. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/RECORD +154 -156
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_stepwise_rewards.py +0 -58
  152. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  153. synth_ai/install_sqld.sh +0 -40
  154. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/WHEEL +0 -0
  155. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/entry_points.txt +0 -0
  156. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/licenses/LICENSE +0 -0
  157. {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev7.dist-info}/top_level.txt +0 -0
@@ -42,6 +42,7 @@ def _health_response_ok(resp: requests.Response | None) -> tuple[bool, str]:
42
42
  def check_task_app_health(base_url: str, api_key: str, *, timeout: float = 10.0) -> TaskAppHealth:
43
43
  # Send ALL known environment keys so the server can authorize any valid one
44
44
  import os
45
+
45
46
  headers = {"X-API-Key": api_key}
46
47
  aliases = (os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
47
48
  keys: list[str] = [api_key]
@@ -146,7 +147,9 @@ def list_modal_secrets(pattern: str | None = None) -> list[str]:
146
147
  def get_modal_secret_value(name: str) -> str:
147
148
  result = _run_modal(["secret", "get", name])
148
149
  if result.code != 0:
149
- raise click.ClickException(f"modal secret get {name} failed: {result.stderr or result.stdout}")
150
+ raise click.ClickException(
151
+ f"modal secret get {name} failed: {result.stderr or result.stdout}"
152
+ )
150
153
  value = result.stdout.strip()
151
154
  if not value:
152
155
  raise click.ClickException(f"Secret {name} is empty")
@@ -83,7 +83,13 @@ class CLIResult:
83
83
  stderr: str
84
84
 
85
85
 
86
- def run_cli(args: Iterable[str], *, cwd: Path | None = None, env: Mapping[str, str] | None = None, timeout: float | None = None) -> CLIResult:
86
+ def run_cli(
87
+ args: Iterable[str],
88
+ *,
89
+ cwd: Path | None = None,
90
+ env: Mapping[str, str] | None = None,
91
+ timeout: float | None = None,
92
+ ) -> CLIResult:
87
93
  proc = subprocess.run(
88
94
  list(args),
89
95
  cwd=cwd,
@@ -95,17 +101,27 @@ def run_cli(args: Iterable[str], *, cwd: Path | None = None, env: Mapping[str, s
95
101
  return CLIResult(code=proc.returncode, stdout=proc.stdout.strip(), stderr=proc.stderr.strip())
96
102
 
97
103
 
98
- def http_post(url: str, *, headers: Mapping[str, str] | None = None, json_body: Any | None = None, timeout: float = 60.0) -> requests.Response:
104
+ def http_post(
105
+ url: str,
106
+ *,
107
+ headers: Mapping[str, str] | None = None,
108
+ json_body: Any | None = None,
109
+ timeout: float = 60.0,
110
+ ) -> requests.Response:
99
111
  resp = requests.post(url, headers=dict(headers or {}), json=json_body, timeout=timeout)
100
112
  return resp
101
113
 
102
114
 
103
- def http_get(url: str, *, headers: Mapping[str, str] | None = None, timeout: float = 30.0) -> requests.Response:
115
+ def http_get(
116
+ url: str, *, headers: Mapping[str, str] | None = None, timeout: float = 30.0
117
+ ) -> requests.Response:
104
118
  resp = requests.get(url, headers=dict(headers or {}), timeout=timeout)
105
119
  return resp
106
120
 
107
121
 
108
- def post_multipart(url: str, *, api_key: str, file_field: str, file_path: Path, purpose: str = "fine-tune") -> requests.Response:
122
+ def post_multipart(
123
+ url: str, *, api_key: str, file_field: str, file_path: Path, purpose: str = "fine-tune"
124
+ ) -> requests.Response:
109
125
  headers = {"Authorization": f"Bearer {api_key}"}
110
126
  files = {file_field: (file_path.name, file_path.read_bytes(), "application/jsonl")}
111
127
  data = {"purpose": purpose}
synth_ai/cli/__init__.py CHANGED
@@ -69,6 +69,12 @@ try:
69
69
  _demo.register(cli)
70
70
  except Exception:
71
71
  pass
72
+ try:
73
+ from . import turso as _turso
74
+
75
+ _turso.register(cli)
76
+ except Exception:
77
+ pass
72
78
  try:
73
79
  from . import rl_demo as _rl_demo
74
80
 
@@ -83,18 +89,19 @@ except Exception:
83
89
  pass
84
90
 
85
91
 
86
-
87
92
  from .task_apps import task_app_group
93
+
88
94
  cli.add_command(task_app_group, name="task-app")
89
95
 
90
96
 
91
97
  try:
92
98
  from . import task_apps as _task_apps
99
+
93
100
  _task_apps.register(cli)
94
101
  except Exception:
95
102
  pass
96
103
 
97
- cli.add_command(task_app_group.commands['serve'], name='serve')
98
- cli.add_command(task_app_group.commands['deploy'], name='deploy')
104
+ cli.add_command(task_app_group.commands["serve"], name="serve")
105
+ cli.add_command(task_app_group.commands["deploy"], name="deploy")
99
106
 
100
- cli.add_command(task_app_group.commands['modal-serve'], name='modal-serve')
107
+ cli.add_command(task_app_group.commands["modal-serve"], name="modal-serve")
synth_ai/cli/balance.py CHANGED
@@ -29,7 +29,7 @@ def _get_default_base_url() -> str:
29
29
  base, _ = get_backend_from_env()
30
30
  base = base.rstrip("/")
31
31
  if base.endswith("/api"):
32
- base = base[:-len("/api")]
32
+ base = base[: -len("/api")]
33
33
  return f"{base}/api/v1"
34
34
 
35
35
 
synth_ai/cli/demo.py CHANGED
@@ -23,6 +23,7 @@ def _find_demo_scripts(root: Path) -> list[Path]:
23
23
 
24
24
  def _forward_to_new(args: list[str]) -> None:
25
25
  import sys
26
+
26
27
  try:
27
28
  from synth_ai.demos.core import cli as demo_cli # type: ignore
28
29
  except Exception as e: # pragma: no cover
@@ -35,7 +36,9 @@ def _forward_to_new(args: list[str]) -> None:
35
36
 
36
37
  def register(cli):
37
38
  @cli.group("demo", invoke_without_command=True)
38
- @click.option("--force", is_flag=True, help="Overwrite existing files in CWD when initializing demo")
39
+ @click.option(
40
+ "--force", is_flag=True, help="Overwrite existing files in CWD when initializing demo"
41
+ )
39
42
  @click.option("--list", "list_only", is_flag=True, help="List available legacy demos and exit")
40
43
  @click.option("-f", "filter_term", default="", help="Filter legacy demos by substring")
41
44
  @click.pass_context
@@ -99,13 +102,24 @@ def register(cli):
99
102
 
100
103
  # Help pyright understand dynamic Click group attributes
101
104
  from typing import Any, cast as _cast
105
+
102
106
  _dg = _cast(Any, demo)
103
107
 
104
108
  @_dg.command("deploy")
105
109
  @click.option("--local", is_flag=True, help="Run local FastAPI instead of Modal deploy")
106
- @click.option("--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy")
110
+ @click.option(
111
+ "--app",
112
+ type=click.Path(),
113
+ default=None,
114
+ help="Path to Modal app.py for uv run modal deploy",
115
+ )
107
116
  @click.option("--name", type=str, default="synth-math-demo", help="Modal app name")
108
- @click.option("--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)")
117
+ @click.option(
118
+ "--script",
119
+ type=click.Path(),
120
+ default=None,
121
+ help="Path to deploy_task_app.sh (optional legacy)",
122
+ )
109
123
  def demo_deploy(local: bool, app: str | None, name: str, script: str | None):
110
124
  args: list[str] = ["rl_demo.deploy"]
111
125
  if local:
@@ -120,11 +134,11 @@ def register(cli):
120
134
 
121
135
  @_dg.command("configure")
122
136
  def demo_configure():
123
- _forward_to_new(["rl_demo.configure"])
137
+ _forward_to_new(["rl_demo.configure"])
124
138
 
125
139
  @_dg.command("setup")
126
140
  def demo_setup():
127
- _forward_to_new(["rl_demo.setup"])
141
+ _forward_to_new(["rl_demo.setup"])
128
142
 
129
143
  @_dg.command("run")
130
144
  @click.option("--batch-size", type=int, default=None)
synth_ai/cli/rl_demo.py CHANGED
@@ -20,6 +20,7 @@ import click
20
20
 
21
21
  def _forward(args: list[str]) -> None:
22
22
  import sys
23
+
23
24
  try:
24
25
  from synth_ai.demos.core import cli as demo_cli # type: ignore
25
26
  except Exception as e: # pragma: no cover
@@ -37,6 +38,7 @@ def register(cli):
37
38
 
38
39
  # Help pyright understand dynamic Click group attributes
39
40
  from typing import Any, cast as _cast
41
+
40
42
  _rlg = _cast(Any, rl_demo)
41
43
 
42
44
  @_rlg.command("setup")
@@ -47,9 +49,19 @@ def register(cli):
47
49
 
48
50
  @_rlg.command("deploy")
49
51
  @click.option("--local", is_flag=True, help="Run local FastAPI instead of Modal deploy")
50
- @click.option("--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy")
52
+ @click.option(
53
+ "--app",
54
+ type=click.Path(),
55
+ default=None,
56
+ help="Path to Modal app.py for uv run modal deploy",
57
+ )
51
58
  @click.option("--name", type=str, default="synth-math-demo", help="Modal app name")
52
- @click.option("--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)")
59
+ @click.option(
60
+ "--script",
61
+ type=click.Path(),
62
+ default=None,
63
+ help="Path to deploy_task_app.sh (optional legacy)",
64
+ )
53
65
  def rl_deploy(local: bool, app: str | None, name: str, script: str | None):
54
66
  args: list[str] = ["rl_demo.deploy"]
55
67
  if local:
@@ -64,7 +76,7 @@ def register(cli):
64
76
 
65
77
  @_rlg.command("configure")
66
78
  def rl_configure():
67
- _forward(["rl_demo.configure"])
79
+ _forward(["rl_demo.configure"])
68
80
 
69
81
  @_rlg.command("init")
70
82
  @click.option("--template", type=str, default=None, help="Template id to instantiate")
@@ -81,13 +93,22 @@ def register(cli):
81
93
  _forward(args)
82
94
 
83
95
  @_rlg.command("run")
84
- @click.option("--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)")
96
+ @click.option(
97
+ "--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)"
98
+ )
85
99
  @click.option("--batch-size", type=int, default=None)
86
100
  @click.option("--group-size", type=int, default=None)
87
101
  @click.option("--model", type=str, default=None)
88
102
  @click.option("--timeout", type=int, default=600)
89
103
  @click.option("--dry-run", is_flag=True, help="Print request body and exit")
90
- def rl_run(config: str | None, batch_size: int | None, group_size: int | None, model: str | None, timeout: int, dry_run: bool):
104
+ def rl_run(
105
+ config: str | None,
106
+ batch_size: int | None,
107
+ group_size: int | None,
108
+ model: str | None,
109
+ timeout: int,
110
+ dry_run: bool,
111
+ ):
91
112
  args = ["rl_demo.run"]
92
113
  if config:
93
114
  args.extend(["--config", config])
@@ -106,19 +127,29 @@ def register(cli):
106
127
  # Dotted aliases (top-level): legacy check → setup
107
128
  @cli.command("rl_demo.check")
108
129
  def rl_check_alias():
109
- _forward(["rl_demo.setup"])
130
+ _forward(["rl_demo.setup"])
110
131
 
111
132
  @cli.command("rl_demo.setup")
112
133
  def rl_setup_alias():
113
- _forward(["rl_demo.setup"])
134
+ _forward(["rl_demo.setup"])
114
135
 
115
136
  # (prepare alias removed)
116
137
 
117
138
  @cli.command("rl_demo.deploy")
118
139
  @click.option("--local", is_flag=True, help="Run local FastAPI instead of Modal deploy")
119
- @click.option("--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy")
140
+ @click.option(
141
+ "--app",
142
+ type=click.Path(),
143
+ default=None,
144
+ help="Path to Modal app.py for uv run modal deploy",
145
+ )
120
146
  @click.option("--name", type=str, default="synth-math-demo", help="Modal app name")
121
- @click.option("--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)")
147
+ @click.option(
148
+ "--script",
149
+ type=click.Path(),
150
+ default=None,
151
+ help="Path to deploy_task_app.sh (optional legacy)",
152
+ )
122
153
  def rl_deploy_alias(local: bool, app: str | None, name: str, script: str | None):
123
154
  args: list[str] = ["rl_demo.deploy"]
124
155
  if local:
@@ -133,7 +164,7 @@ def register(cli):
133
164
 
134
165
  @cli.command("rl_demo.configure")
135
166
  def rl_configure_alias():
136
- _forward(["rl_demo.configure"])
167
+ _forward(["rl_demo.configure"])
137
168
 
138
169
  @cli.command("rl_demo.init")
139
170
  @click.option("--template", type=str, default=None, help="Template id to instantiate")
@@ -150,13 +181,22 @@ def register(cli):
150
181
  _forward(args)
151
182
 
152
183
  @cli.command("rl_demo.run")
153
- @click.option("--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)")
184
+ @click.option(
185
+ "--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)"
186
+ )
154
187
  @click.option("--batch-size", type=int, default=None)
155
188
  @click.option("--group-size", type=int, default=None)
156
189
  @click.option("--model", type=str, default=None)
157
190
  @click.option("--timeout", type=int, default=600)
158
191
  @click.option("--dry-run", is_flag=True, help="Print request body and exit")
159
- def rl_run_alias(config: str | None, batch_size: int | None, group_size: int | None, model: str | None, timeout: int, dry_run: bool):
192
+ def rl_run_alias(
193
+ config: str | None,
194
+ batch_size: int | None,
195
+ group_size: int | None,
196
+ model: str | None,
197
+ timeout: int,
198
+ dry_run: bool,
199
+ ):
160
200
  args = ["rl_demo.run"]
161
201
  if config:
162
202
  args.extend(["--config", config])
@@ -175,9 +215,19 @@ def register(cli):
175
215
  # Top-level convenience alias: `synth-ai deploy`
176
216
  @cli.command("demo-deploy")
177
217
  @click.option("--local", is_flag=True, help="Run local FastAPI instead of Modal deploy")
178
- @click.option("--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy")
218
+ @click.option(
219
+ "--app",
220
+ type=click.Path(),
221
+ default=None,
222
+ help="Path to Modal app.py for uv run modal deploy",
223
+ )
179
224
  @click.option("--name", type=str, default="synth-math-demo", help="Modal app name")
180
- @click.option("--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)")
225
+ @click.option(
226
+ "--script",
227
+ type=click.Path(),
228
+ default=None,
229
+ help="Path to deploy_task_app.sh (optional legacy)",
230
+ )
181
231
  def deploy_demo(local: bool, app: str | None, name: str, script: str | None):
182
232
  args: list[str] = ["rl_demo.deploy"]
183
233
  if local:
@@ -191,13 +241,22 @@ def register(cli):
191
241
  _forward(args)
192
242
 
193
243
  @cli.command("run")
194
- @click.option("--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)")
244
+ @click.option(
245
+ "--config", type=click.Path(), default=None, help="Path to TOML config (skip prompt)"
246
+ )
195
247
  @click.option("--batch-size", type=int, default=None)
196
248
  @click.option("--group-size", type=int, default=None)
197
249
  @click.option("--model", type=str, default=None)
198
250
  @click.option("--timeout", type=int, default=600)
199
251
  @click.option("--dry-run", is_flag=True, help="Print request body and exit")
200
- def run_top(config: str | None, batch_size: int | None, group_size: int | None, model: str | None, timeout: int, dry_run: bool):
252
+ def run_top(
253
+ config: str | None,
254
+ batch_size: int | None,
255
+ group_size: int | None,
256
+ model: str | None,
257
+ timeout: int,
258
+ dry_run: bool,
259
+ ):
201
260
  args = ["run"]
202
261
  if config:
203
262
  args.extend(["--config", config])
synth_ai/cli/root.py CHANGED
@@ -9,13 +9,17 @@ import logging
9
9
  import os
10
10
  import shutil
11
11
  import signal
12
+ import socket
12
13
  import subprocess
13
14
  import sys
15
+ import tempfile
14
16
  import time
15
17
 
16
18
  import click
19
+
17
20
  try:
18
21
  from importlib.metadata import PackageNotFoundError, version as _pkg_version
22
+
19
23
  try:
20
24
  __pkg_version__ = _pkg_version("synth-ai")
21
25
  except PackageNotFoundError:
@@ -30,6 +34,9 @@ except Exception:
30
34
  __pkg_version__ = "unknown"
31
35
 
32
36
 
37
+ SQLD_VERSION = "v0.26.2"
38
+
39
+
33
40
  def find_sqld_binary() -> str | None:
34
41
  sqld_path = shutil.which("sqld")
35
42
  if sqld_path:
@@ -39,6 +46,7 @@ def find_sqld_binary() -> str | None:
39
46
  "/usr/bin/sqld",
40
47
  os.path.expanduser("~/.local/bin/sqld"),
41
48
  os.path.expanduser("~/bin/sqld"),
49
+ os.path.expanduser("~/.turso/bin/sqld"),
42
50
  ]
43
51
  for path in common_paths:
44
52
  if os.path.exists(path) and os.access(path, os.X_OK):
@@ -47,40 +55,104 @@ def find_sqld_binary() -> str | None:
47
55
 
48
56
 
49
57
  def install_sqld() -> str:
50
- click.echo("🔧 sqld not found. Installing...")
51
- script = """#!/bin/bash
52
- set -e
53
- SQLD_VERSION="v0.26.2"
54
- OS=$(uname -s | tr '[:upper:]' '[:lower:]')
55
- ARCH=$(uname -m)
56
- case "$ARCH" in
57
- x86_64) ARCH="x86_64" ;;
58
- aarch64|arm64) ARCH="aarch64" ;;
59
- *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
60
- esac
61
- URL="https://github.com/tursodatabase/libsql/releases/download/libsql-server-${SQLD_VERSION}/sqld-${OS}-${ARCH}.tar.xz"
62
- TMP_DIR=$(mktemp -d)
63
- cd "$TMP_DIR"
64
- curl -L -o sqld.tar.xz "$URL"
65
- tar -xf sqld.tar.xz
66
- mkdir -p ~/.local/bin
67
- mv sqld ~/.local/bin/
68
- chmod +x ~/.local/bin/sqld
69
- cd -
70
- rm -rf "$TMP_DIR"
71
- """
72
- path = "/tmp/install_sqld.sh"
73
- with open(path, "w") as f:
74
- f.write(script)
75
- subprocess.run(["bash", path], check=True)
76
- os.unlink(path)
77
- local_bin = os.path.expanduser("~/.local/bin")
78
- if local_bin not in os.environ.get("PATH", ""):
79
- os.environ["PATH"] = f"{local_bin}:{os.environ.get('PATH', '')}"
80
- return os.path.expanduser("~/.local/bin/sqld")
81
-
82
-
83
- @click.group(help=f"Synth AI v{__pkg_version__} - Software for aiding the best and multiplying the will.")
58
+ """Install sqld via the Turso CLI, installing the CLI via Homebrew if needed."""
59
+
60
+ click.echo("🔧 sqld not found. Attempting automatic install...")
61
+
62
+ turso_cli_path = shutil.which("turso")
63
+ brew_path = shutil.which("brew")
64
+
65
+ if not turso_cli_path:
66
+ if not brew_path:
67
+ raise click.ClickException(
68
+ "Automatic install requires either Homebrew or an existing Turso CLI.\n"
69
+ "Install manually using one of:\n"
70
+ " • brew install tursodatabase/tap/turso\n"
71
+ " • curl -sSfL https://get.tur.so/install.sh | bash\n"
72
+ "Then run 'turso dev' once and re-run this command."
73
+ )
74
+
75
+ click.echo("🧰 Installing Turso CLI via Homebrew (tursodatabase/tap/turso)…")
76
+ try:
77
+ subprocess.run(
78
+ [brew_path, "install", "tursodatabase/tap/turso"],
79
+ check=True,
80
+ )
81
+ except subprocess.CalledProcessError as exc:
82
+ raise click.ClickException(
83
+ "Homebrew install failed. Please resolve brew errors and retry."
84
+ ) from exc
85
+
86
+ turso_cli_path = shutil.which("turso")
87
+ if not turso_cli_path:
88
+ raise click.ClickException(
89
+ "Homebrew reported success but the 'turso' binary is not on PATH."
90
+ )
91
+
92
+ click.echo("📥 Downloading sqld via 'turso dev' (this may take a few seconds)…")
93
+
94
+ temp_db = tempfile.NamedTemporaryFile(prefix="synth_sqld_", suffix=".db", delete=False)
95
+ temp_db_path = temp_db.name
96
+ temp_db.close()
97
+
98
+ env = os.environ.copy()
99
+ env.setdefault("TURSO_NONINTERACTIVE", "1")
100
+
101
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
102
+ sock.bind(("127.0.0.1", 0))
103
+ port = sock.getsockname()[1]
104
+
105
+ cmd = [
106
+ turso_cli_path,
107
+ "dev",
108
+ f"--db-file={temp_db_path}",
109
+ f"--port={port}",
110
+ ]
111
+ proc: subprocess.Popen[str] | None = None
112
+ stdout_data = ""
113
+ stderr_data = ""
114
+ try:
115
+ proc = subprocess.Popen(
116
+ cmd,
117
+ stdout=subprocess.PIPE,
118
+ stderr=subprocess.PIPE,
119
+ text=True,
120
+ env=env,
121
+ )
122
+ try:
123
+ stdout_data, stderr_data = proc.communicate(timeout=10)
124
+ except subprocess.TimeoutExpired:
125
+ proc.terminate()
126
+ try:
127
+ stdout_data, stderr_data = proc.communicate(timeout=5)
128
+ except subprocess.TimeoutExpired:
129
+ proc.kill()
130
+ stdout_data, stderr_data = proc.communicate()
131
+ finally:
132
+ if proc and proc.returncode not in (0, None):
133
+ if stdout_data or stderr_data:
134
+ logging.getLogger(__name__).debug(
135
+ "turso dev stdout: %s\nstderr: %s", stdout_data, stderr_data
136
+ )
137
+ try:
138
+ os.unlink(temp_db_path)
139
+ except OSError:
140
+ pass
141
+
142
+ sqld_path = find_sqld_binary()
143
+ if sqld_path:
144
+ click.echo(f"✅ sqld available at {sqld_path}")
145
+ return sqld_path
146
+
147
+ raise click.ClickException(
148
+ "sqld download did not succeed. Run 'turso dev' manually once, "
149
+ "ensure it downloads sqld, and try again."
150
+ )
151
+
152
+
153
+ @click.group(
154
+ help=f"Synth AI v{__pkg_version__} - Software for aiding the best and multiplying the will."
155
+ )
84
156
  @click.version_option(version=__pkg_version__, prog_name="synth-ai")
85
157
  def cli():
86
158
  """Top-level command group for Synth AI."""
@@ -109,9 +181,13 @@ def _forward_to_demo(args: list[str]) -> None:
109
181
 
110
182
  @demo.command()
111
183
  @click.option("--local", is_flag=True, help="Run local FastAPI instead of Modal deploy")
112
- @click.option("--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy")
184
+ @click.option(
185
+ "--app", type=click.Path(), default=None, help="Path to Modal app.py for uv run modal deploy"
186
+ )
113
187
  @click.option("--name", type=str, default="synth-math-demo", help="Modal app name")
114
- @click.option("--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)")
188
+ @click.option(
189
+ "--script", type=click.Path(), default=None, help="Path to deploy_task_app.sh (optional legacy)"
190
+ )
115
191
  def deploy(local: bool, app: str | None, name: str, script: str | None):
116
192
  """Deploy the Math Task App (Modal by default)."""
117
193
  args: list[str] = ["rl_demo.deploy"]
@@ -205,7 +281,10 @@ def serve_deprecated(
205
281
  force: bool,
206
282
  ):
207
283
  logging.basicConfig(level=logging.INFO, format="%(message)s")
208
- click.echo("⚠️ 'synth-ai serve' now targets task apps; use 'synth-ai serve' for task apps or 'synth-ai serve-deprecated' for this legacy service.", err=True)
284
+ click.echo(
285
+ "⚠️ 'synth-ai serve' now targets task apps; use 'synth-ai serve' for task apps or 'synth-ai serve-deprecated' for this legacy service.",
286
+ err=True,
287
+ )
209
288
  processes = []
210
289
 
211
290
  def signal_handler(sig, frame):