synth-ai 0.2.8.dev2__py3-none-any.whl → 0.4.3__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.
Files changed (740) hide show
  1. synth_ai/__init__.py +44 -24
  2. synth_ai/__main__.py +30 -3
  3. synth_ai/cli/__init__.py +103 -48
  4. synth_ai/cli/__main__.py +42 -0
  5. synth_ai/cli/_internal/__init__.py +5 -0
  6. synth_ai/cli/_internal/modal_wrapper.py +31 -0
  7. synth_ai/cli/_internal/storage.py +20 -0
  8. synth_ai/cli/_internal/typer_patch.py +47 -0
  9. synth_ai/cli/_internal/validate_task_app.py +29 -0
  10. synth_ai/cli/agents/__init__.py +17 -0
  11. synth_ai/cli/agents/claude.py +77 -0
  12. synth_ai/cli/agents/codex.py +265 -0
  13. synth_ai/cli/agents/opencode.py +253 -0
  14. synth_ai/cli/commands/__init__.py +18 -0
  15. synth_ai/cli/commands/artifacts/__init__.py +13 -0
  16. synth_ai/cli/commands/artifacts/client.py +119 -0
  17. synth_ai/cli/commands/artifacts/config.py +57 -0
  18. synth_ai/cli/commands/artifacts/core.py +24 -0
  19. synth_ai/cli/commands/artifacts/download.py +188 -0
  20. synth_ai/cli/commands/artifacts/export.py +186 -0
  21. synth_ai/cli/commands/artifacts/list.py +156 -0
  22. synth_ai/cli/commands/artifacts/parsing.py +250 -0
  23. synth_ai/cli/commands/artifacts/show.py +336 -0
  24. synth_ai/cli/commands/demo/__init__.py +3 -0
  25. synth_ai/cli/commands/demo/core.py +153 -0
  26. synth_ai/cli/commands/eval/__init__.py +10 -0
  27. synth_ai/cli/commands/eval/config.py +338 -0
  28. synth_ai/cli/commands/eval/core.py +256 -0
  29. synth_ai/cli/commands/eval/runner.py +704 -0
  30. synth_ai/cli/commands/eval/validation.py +60 -0
  31. synth_ai/cli/commands/filter/__init__.py +12 -0
  32. synth_ai/cli/commands/filter/core.py +424 -0
  33. synth_ai/cli/commands/filter/errors.py +55 -0
  34. synth_ai/cli/commands/filter/validation.py +77 -0
  35. synth_ai/cli/commands/help/__init__.py +185 -0
  36. synth_ai/cli/commands/help/core.py +72 -0
  37. synth_ai/cli/commands/scan/__init__.py +19 -0
  38. synth_ai/cli/commands/scan/cloudflare_scanner.py +403 -0
  39. synth_ai/cli/commands/scan/core.py +344 -0
  40. synth_ai/cli/commands/scan/health_checker.py +242 -0
  41. synth_ai/cli/commands/scan/local_scanner.py +278 -0
  42. synth_ai/cli/commands/scan/models.py +83 -0
  43. synth_ai/cli/commands/smoke/__init__.py +7 -0
  44. synth_ai/cli/commands/smoke/core.py +1428 -0
  45. synth_ai/cli/commands/status/__init__.py +3 -0
  46. synth_ai/cli/commands/status/client.py +91 -0
  47. synth_ai/cli/commands/status/config.py +12 -0
  48. synth_ai/cli/commands/status/errors.py +11 -0
  49. synth_ai/cli/commands/status/subcommands/__init__.py +3 -0
  50. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  51. synth_ai/cli/commands/status/subcommands/files.py +34 -0
  52. synth_ai/cli/commands/status/subcommands/jobs.py +51 -0
  53. synth_ai/cli/commands/status/subcommands/models.py +35 -0
  54. synth_ai/cli/commands/status/subcommands/runs.py +34 -0
  55. synth_ai/cli/commands/status/subcommands/session.py +77 -0
  56. synth_ai/cli/commands/status/subcommands/summary.py +39 -0
  57. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  58. synth_ai/cli/commands/status/utils.py +23 -0
  59. synth_ai/cli/commands/train/__init__.py +53 -0
  60. synth_ai/cli/commands/train/core.py +22 -0
  61. synth_ai/cli/commands/train/errors.py +117 -0
  62. synth_ai/cli/commands/train/judge_schemas.py +201 -0
  63. synth_ai/cli/commands/train/judge_validation.py +305 -0
  64. synth_ai/cli/commands/train/prompt_learning_validation.py +633 -0
  65. synth_ai/cli/commands/train/validation.py +392 -0
  66. synth_ai/cli/demo_apps/__init__.py +10 -0
  67. synth_ai/cli/demo_apps/core/__init__.py +28 -0
  68. synth_ai/{demos → cli/demo_apps}/core/cli.py +783 -441
  69. synth_ai/cli/demo_apps/crafter/__init__.py +1 -0
  70. synth_ai/cli/demo_apps/crafter/crafter_fft_4b.toml +55 -0
  71. synth_ai/cli/demo_apps/crafter/grpo_crafter_task_app.py +186 -0
  72. synth_ai/cli/demo_apps/crafter/rl_from_base_qwen4b.toml +74 -0
  73. synth_ai/cli/demo_apps/demo_registry.py +176 -0
  74. synth_ai/cli/demo_apps/demo_task_apps/__init__.py +7 -0
  75. synth_ai/{demos → cli/demo_apps}/demo_task_apps/core.py +75 -37
  76. synth_ai/cli/demo_apps/demo_task_apps/crafter/__init__.py +1 -0
  77. synth_ai/cli/demo_apps/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
  78. synth_ai/cli/demo_apps/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
  79. synth_ai/cli/demo_apps/demo_task_apps/crafter/grpo_crafter_task_app.py +185 -0
  80. synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/_common.py +1 -2
  81. synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/app.py +2 -1
  82. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +73 -0
  83. synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/deploy_modal.py +3 -6
  84. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +738 -0
  85. synth_ai/cli/demo_apps/demo_task_apps/math/task_app_entry.py +39 -0
  86. synth_ai/cli/demo_apps/math/__init__.py +1 -0
  87. synth_ai/cli/demo_apps/math/_common.py +16 -0
  88. synth_ai/cli/demo_apps/math/app.py +38 -0
  89. synth_ai/cli/demo_apps/math/config.toml +75 -0
  90. synth_ai/cli/demo_apps/math/deploy_modal.py +54 -0
  91. synth_ai/cli/demo_apps/math/modal_task_app.py +698 -0
  92. synth_ai/cli/demo_apps/math/task_app_entry.py +53 -0
  93. synth_ai/cli/demo_apps/mipro/main.py +271 -0
  94. synth_ai/cli/demo_apps/mipro/task_app.py +922 -0
  95. synth_ai/cli/demo_apps/mipro/train_cfg.toml +92 -0
  96. synth_ai/cli/demos/__init__.py +12 -0
  97. synth_ai/cli/demos/demo.py +32 -0
  98. synth_ai/cli/demos/rl_demo.py +254 -0
  99. synth_ai/cli/deploy.py +216 -0
  100. synth_ai/cli/infra/__init__.py +14 -0
  101. synth_ai/cli/{balance.py → infra/balance.py} +16 -4
  102. synth_ai/cli/infra/mcp.py +35 -0
  103. synth_ai/cli/infra/modal_app.py +36 -0
  104. synth_ai/cli/infra/setup.py +69 -0
  105. synth_ai/cli/infra/status.py +16 -0
  106. synth_ai/cli/infra/turso.py +77 -0
  107. synth_ai/cli/lib/__init__.py +10 -0
  108. synth_ai/cli/lib/agents.py +76 -0
  109. synth_ai/cli/lib/apps/modal_app.py +101 -0
  110. synth_ai/cli/lib/apps/task_app.py +642 -0
  111. synth_ai/cli/lib/bin.py +39 -0
  112. synth_ai/cli/lib/env.py +375 -0
  113. synth_ai/cli/lib/errors.py +85 -0
  114. synth_ai/cli/lib/modal.py +315 -0
  115. synth_ai/cli/lib/plotting.py +126 -0
  116. synth_ai/cli/lib/prompt_args.py +39 -0
  117. synth_ai/cli/lib/prompts.py +284 -0
  118. synth_ai/cli/lib/sqld.py +122 -0
  119. synth_ai/cli/lib/task_app_discovery.py +884 -0
  120. synth_ai/cli/lib/task_app_env.py +295 -0
  121. synth_ai/cli/lib/train_cfgs.py +300 -0
  122. synth_ai/cli/lib/tunnel_records.py +207 -0
  123. synth_ai/cli/local/__init__.py +14 -0
  124. synth_ai/cli/local/experiment_queue/__init__.py +72 -0
  125. synth_ai/cli/local/experiment_queue/api_schemas.py +221 -0
  126. synth_ai/cli/local/experiment_queue/celery_app.py +208 -0
  127. synth_ai/cli/local/experiment_queue/config.py +128 -0
  128. synth_ai/cli/local/experiment_queue/config_utils.py +272 -0
  129. synth_ai/cli/local/experiment_queue/database.py +175 -0
  130. synth_ai/cli/local/experiment_queue/dispatcher.py +119 -0
  131. synth_ai/cli/local/experiment_queue/models.py +231 -0
  132. synth_ai/cli/local/experiment_queue/progress_info.py +160 -0
  133. synth_ai/cli/local/experiment_queue/results.py +373 -0
  134. synth_ai/cli/local/experiment_queue/schemas.py +131 -0
  135. synth_ai/cli/local/experiment_queue/service.py +344 -0
  136. synth_ai/cli/local/experiment_queue/status.py +372 -0
  137. synth_ai/cli/local/experiment_queue/status_tracker.py +360 -0
  138. synth_ai/cli/local/experiment_queue/tasks.py +1984 -0
  139. synth_ai/cli/local/experiment_queue/trace_storage.py +65 -0
  140. synth_ai/cli/local/experiment_queue/validation.py +157 -0
  141. synth_ai/cli/local/session/__init__.py +92 -0
  142. synth_ai/cli/local/session/client.py +383 -0
  143. synth_ai/cli/local/session/constants.py +63 -0
  144. synth_ai/cli/local/session/exceptions.py +105 -0
  145. synth_ai/cli/local/session/manager.py +139 -0
  146. synth_ai/cli/local/session/models.py +89 -0
  147. synth_ai/cli/local/session/query.py +110 -0
  148. synth_ai/cli/root.py +150 -108
  149. synth_ai/cli/task_apps/__init__.py +37 -0
  150. synth_ai/cli/task_apps/commands.py +3145 -0
  151. synth_ai/cli/task_apps/deploy.py +7 -0
  152. synth_ai/cli/task_apps/list.py +26 -0
  153. synth_ai/cli/task_apps/main.py +36 -0
  154. synth_ai/cli/task_apps/modal_serve.py +11 -0
  155. synth_ai/cli/task_apps/serve.py +11 -0
  156. synth_ai/cli/training/__init__.py +8 -0
  157. synth_ai/cli/training/train.py +5 -0
  158. synth_ai/cli/training/train_cfg.py +34 -0
  159. synth_ai/cli/{watch.py → training/watch.py} +13 -18
  160. synth_ai/cli/turso.py +52 -0
  161. synth_ai/cli/utils/__init__.py +8 -0
  162. synth_ai/cli/utils/experiments.py +235 -0
  163. synth_ai/cli/utils/queue.py +504 -0
  164. synth_ai/cli/{recent.py → utils/recent.py} +13 -7
  165. synth_ai/cli/{traces.py → utils/traces.py} +9 -5
  166. synth_ai/contracts/__init__.py +67 -0
  167. synth_ai/core/__init__.py +100 -0
  168. synth_ai/core/_utils/__init__.py +54 -0
  169. synth_ai/core/_utils/base_url.py +10 -0
  170. synth_ai/core/_utils/http.py +10 -0
  171. synth_ai/core/_utils/prompts.py +14 -0
  172. synth_ai/core/_utils/task_app_state.py +12 -0
  173. synth_ai/core/_utils/user_config.py +10 -0
  174. synth_ai/core/apps/common.py +116 -0
  175. synth_ai/core/auth.py +95 -0
  176. synth_ai/core/cfgs.py +240 -0
  177. synth_ai/core/config/__init__.py +16 -0
  178. synth_ai/core/config/base.py +168 -0
  179. synth_ai/core/config/resolver.py +89 -0
  180. synth_ai/core/env.py +231 -0
  181. synth_ai/core/errors.py +126 -0
  182. synth_ai/core/http.py +230 -0
  183. synth_ai/core/integrations/__init__.py +11 -0
  184. synth_ai/core/integrations/cloudflare.py +1710 -0
  185. synth_ai/core/integrations/mcp/__init__.py +6 -0
  186. synth_ai/core/integrations/mcp/__main__.py +8 -0
  187. synth_ai/core/integrations/mcp/claude.py +36 -0
  188. synth_ai/core/integrations/mcp/main.py +254 -0
  189. synth_ai/core/integrations/mcp/setup.py +100 -0
  190. synth_ai/core/integrations/modal.py +277 -0
  191. synth_ai/core/json.py +72 -0
  192. synth_ai/core/log_filter.py +99 -0
  193. synth_ai/core/logging.py +82 -0
  194. synth_ai/core/paths.py +107 -0
  195. synth_ai/core/pricing.py +109 -0
  196. synth_ai/core/process.py +233 -0
  197. synth_ai/core/ssl.py +25 -0
  198. synth_ai/core/storage/__init__.py +71 -0
  199. synth_ai/core/task_app_state.py +318 -0
  200. synth_ai/core/telemetry.py +282 -0
  201. synth_ai/{tracing_v3 → core/tracing_v3}/__init__.py +5 -1
  202. synth_ai/{tracing_v3 → core/tracing_v3}/abstractions.py +21 -4
  203. synth_ai/core/tracing_v3/config.py +229 -0
  204. synth_ai/core/tracing_v3/constants.py +21 -0
  205. synth_ai/{tracing_v3 → core/tracing_v3}/db_config.py +42 -29
  206. synth_ai/{tracing_v3 → core/tracing_v3}/decorators.py +80 -45
  207. synth_ai/{tracing_v3 → core/tracing_v3}/examples/basic_usage.py +15 -9
  208. synth_ai/{tracing_v3 → core/tracing_v3}/hooks.py +6 -4
  209. synth_ai/{tracing_v3 → core/tracing_v3}/llm_call_record_helpers.py +161 -61
  210. synth_ai/{tracing_v3 → core/tracing_v3}/migration_helper.py +1 -2
  211. synth_ai/{tracing_v3 → core/tracing_v3}/replica_sync.py +12 -7
  212. synth_ai/core/tracing_v3/serialization.py +130 -0
  213. synth_ai/{tracing_v3 → core/tracing_v3}/session_tracer.py +88 -21
  214. synth_ai/{tracing_v3 → core/tracing_v3}/storage/base.py +99 -12
  215. synth_ai/core/tracing_v3/storage/config.py +109 -0
  216. synth_ai/{tracing_v3 → core/tracing_v3}/storage/factory.py +11 -9
  217. synth_ai/{tracing_v3 → core/tracing_v3}/storage/utils.py +15 -11
  218. synth_ai/core/tracing_v3/trace_utils.py +326 -0
  219. synth_ai/core/tracing_v3/turso/__init__.py +12 -0
  220. synth_ai/core/tracing_v3/turso/daemon.py +278 -0
  221. synth_ai/{tracing_v3 → core/tracing_v3}/turso/models.py +7 -3
  222. synth_ai/core/tracing_v3/turso/native_manager.py +1385 -0
  223. synth_ai/{tracing_v3 → core/tracing_v3}/utils.py +5 -4
  224. synth_ai/core/urls.py +18 -0
  225. synth_ai/core/user_config.py +137 -0
  226. synth_ai/core/uvicorn.py +222 -0
  227. synth_ai/data/__init__.py +83 -0
  228. synth_ai/data/enums.py +123 -0
  229. synth_ai/data/rewards.py +152 -0
  230. synth_ai/data/traces.py +35 -0
  231. synth_ai/products/__init__.py +6 -0
  232. synth_ai/products/graph_evolve/__init__.py +46 -0
  233. synth_ai/products/graph_evolve/client.py +226 -0
  234. synth_ai/products/graph_evolve/config.py +591 -0
  235. synth_ai/products/graph_evolve/converters/__init__.py +42 -0
  236. synth_ai/products/graph_evolve/converters/openai_sft.py +484 -0
  237. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +109 -0
  238. synth_ai/products/graph_evolve/run.py +222 -0
  239. synth_ai/products/graph_gepa/__init__.py +23 -0
  240. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  241. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  242. synth_ai/sdk/__init__.py +123 -0
  243. synth_ai/sdk/api/__init__.py +1 -0
  244. synth_ai/sdk/api/models/supported.py +514 -0
  245. synth_ai/sdk/api/research_agent/__init__.py +296 -0
  246. synth_ai/sdk/api/train/__init__.py +85 -0
  247. synth_ai/sdk/api/train/builders.py +895 -0
  248. synth_ai/sdk/api/train/cli.py +2199 -0
  249. synth_ai/sdk/api/train/config_finder.py +267 -0
  250. synth_ai/sdk/api/train/configs/__init__.py +65 -0
  251. synth_ai/sdk/api/train/configs/prompt_learning.py +1706 -0
  252. synth_ai/sdk/api/train/configs/rl.py +187 -0
  253. synth_ai/sdk/api/train/configs/sft.py +99 -0
  254. synth_ai/sdk/api/train/configs/shared.py +81 -0
  255. synth_ai/sdk/api/train/context_learning.py +312 -0
  256. synth_ai/sdk/api/train/env_resolver.py +418 -0
  257. synth_ai/sdk/api/train/graph_validators.py +216 -0
  258. synth_ai/sdk/api/train/graphgen.py +984 -0
  259. synth_ai/sdk/api/train/graphgen_models.py +823 -0
  260. synth_ai/sdk/api/train/graphgen_validators.py +109 -0
  261. synth_ai/sdk/api/train/local_api.py +10 -0
  262. synth_ai/sdk/api/train/pollers.py +124 -0
  263. synth_ai/sdk/api/train/progress/__init__.py +97 -0
  264. synth_ai/sdk/api/train/progress/dataclasses.py +569 -0
  265. synth_ai/sdk/api/train/progress/events.py +326 -0
  266. synth_ai/sdk/api/train/progress/results.py +428 -0
  267. synth_ai/sdk/api/train/progress/tracker.py +641 -0
  268. synth_ai/sdk/api/train/prompt_learning.py +469 -0
  269. synth_ai/sdk/api/train/rl.py +441 -0
  270. synth_ai/sdk/api/train/sft.py +396 -0
  271. synth_ai/sdk/api/train/summary.py +522 -0
  272. synth_ai/sdk/api/train/supported_algos.py +147 -0
  273. synth_ai/sdk/api/train/task_app.py +351 -0
  274. synth_ai/sdk/api/train/utils.py +279 -0
  275. synth_ai/sdk/api/train/validators.py +2424 -0
  276. synth_ai/sdk/graphs/__init__.py +15 -0
  277. synth_ai/sdk/graphs/completions.py +570 -0
  278. synth_ai/{inference → sdk/inference}/__init__.py +0 -1
  279. synth_ai/sdk/inference/client.py +128 -0
  280. synth_ai/sdk/jobs/__init__.py +16 -0
  281. synth_ai/sdk/jobs/client.py +371 -0
  282. synth_ai/sdk/judging/__init__.py +14 -0
  283. synth_ai/sdk/judging/base.py +24 -0
  284. synth_ai/sdk/judging/client.py +40 -0
  285. synth_ai/sdk/judging/schemas.py +222 -0
  286. synth_ai/sdk/judging/types.py +42 -0
  287. synth_ai/sdk/learning/__init__.py +99 -0
  288. synth_ai/sdk/learning/algorithms.py +14 -0
  289. synth_ai/{learning → sdk/learning}/client.py +121 -30
  290. synth_ai/sdk/learning/config.py +5 -0
  291. synth_ai/{learning → sdk/learning}/constants.py +0 -2
  292. synth_ai/sdk/learning/context_learning_client.py +531 -0
  293. synth_ai/sdk/learning/context_learning_types.py +292 -0
  294. synth_ai/sdk/learning/ft_client.py +7 -0
  295. synth_ai/{learning → sdk/learning}/health.py +15 -9
  296. synth_ai/{learning → sdk/learning}/jobs.py +44 -47
  297. synth_ai/sdk/learning/prompt_extraction.py +334 -0
  298. synth_ai/sdk/learning/prompt_learning_client.py +455 -0
  299. synth_ai/sdk/learning/prompt_learning_types.py +186 -0
  300. synth_ai/{rl → sdk/learning/rl}/__init__.py +13 -8
  301. synth_ai/{learning/rl_client.py → sdk/learning/rl/client.py} +89 -77
  302. synth_ai/sdk/learning/rl/config.py +31 -0
  303. synth_ai/{rl → sdk/learning/rl}/contracts.py +5 -14
  304. synth_ai/{rl → sdk/learning/rl}/env_keys.py +45 -16
  305. synth_ai/sdk/learning/rl/secrets.py +13 -0
  306. synth_ai/sdk/learning/rl_client.py +5 -0
  307. synth_ai/sdk/learning/sft/__init__.py +29 -0
  308. synth_ai/sdk/learning/sft/client.py +95 -0
  309. synth_ai/sdk/learning/sft/config.py +270 -0
  310. synth_ai/sdk/learning/sft/data.py +698 -0
  311. synth_ai/sdk/learning/sse.py +57 -0
  312. synth_ai/sdk/learning/validators.py +52 -0
  313. synth_ai/sdk/localapi/__init__.py +40 -0
  314. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  315. synth_ai/sdk/localapi/client.py +10 -0
  316. synth_ai/sdk/localapi/contracts.py +10 -0
  317. synth_ai/sdk/localapi/helpers.py +519 -0
  318. synth_ai/sdk/localapi/rollouts.py +87 -0
  319. synth_ai/sdk/localapi/server.py +29 -0
  320. synth_ai/sdk/localapi/template.py +70 -0
  321. synth_ai/sdk/streaming/__init__.py +35 -0
  322. synth_ai/sdk/streaming/config.py +94 -0
  323. synth_ai/sdk/streaming/handlers.py +1997 -0
  324. synth_ai/sdk/streaming/streamer.py +713 -0
  325. synth_ai/sdk/streaming/types.py +112 -0
  326. synth_ai/sdk/task/__init__.py +164 -0
  327. synth_ai/sdk/task/apps/__init__.py +169 -0
  328. synth_ai/sdk/task/auth.py +165 -0
  329. synth_ai/sdk/task/client.py +175 -0
  330. synth_ai/sdk/task/config.py +257 -0
  331. synth_ai/sdk/task/contracts.py +219 -0
  332. synth_ai/sdk/task/datasets.py +108 -0
  333. synth_ai/sdk/task/errors.py +50 -0
  334. synth_ai/sdk/task/health.py +34 -0
  335. synth_ai/sdk/task/in_process.py +1190 -0
  336. synth_ai/sdk/task/in_process_runner.py +314 -0
  337. synth_ai/sdk/task/inference_api.py +299 -0
  338. synth_ai/sdk/task/json.py +111 -0
  339. synth_ai/sdk/task/proxy.py +287 -0
  340. synth_ai/sdk/task/rubrics/__init__.py +55 -0
  341. synth_ai/sdk/task/rubrics/loaders.py +156 -0
  342. synth_ai/sdk/task/rubrics/models.py +57 -0
  343. synth_ai/sdk/task/rubrics/scoring.py +116 -0
  344. synth_ai/sdk/task/rubrics/strict.py +149 -0
  345. synth_ai/sdk/task/rubrics.py +219 -0
  346. synth_ai/sdk/task/server.py +631 -0
  347. synth_ai/sdk/task/trace_correlation_helpers.py +539 -0
  348. synth_ai/sdk/task/tracing_utils.py +95 -0
  349. synth_ai/sdk/task/validators.py +441 -0
  350. synth_ai/sdk/task/vendors.py +59 -0
  351. synth_ai/sdk/training/__init__.py +102 -0
  352. synth_ai/sdk/tunnels/__init__.py +83 -0
  353. synth_ai/sdk/tunnels/cleanup.py +83 -0
  354. synth_ai/sdk/tunnels/ports.py +120 -0
  355. synth_ai/utils/__init__.py +213 -0
  356. synth_ai-0.4.3.dist-info/METADATA +262 -0
  357. synth_ai-0.4.3.dist-info/RECORD +370 -0
  358. {synth_ai-0.2.8.dev2.dist-info → synth_ai-0.4.3.dist-info}/entry_points.txt +0 -1
  359. synth_ai/cli/calc.py +0 -69
  360. synth_ai/cli/demo.py +0 -144
  361. synth_ai/cli/legacy_root_backup.py +0 -470
  362. synth_ai/cli/man.py +0 -106
  363. synth_ai/cli/rl_demo.py +0 -202
  364. synth_ai/cli/status.py +0 -133
  365. synth_ai/config/base_url.py +0 -107
  366. synth_ai/core/experiment.py +0 -15
  367. synth_ai/core/system.py +0 -15
  368. synth_ai/demos/core/__init__.py +0 -1
  369. synth_ai/demos/demo_task_apps/__init__.py +0 -1
  370. synth_ai/demos/demo_task_apps/math/config.toml +0 -129
  371. synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +0 -22
  372. synth_ai/demos/demo_task_apps/math/modal_task_app.py +0 -415
  373. synth_ai/environments/__init__.py +0 -31
  374. synth_ai/environments/environment/__init__.py +0 -1
  375. synth_ai/environments/environment/artifacts/__init__.py +0 -1
  376. synth_ai/environments/environment/artifacts/base.py +0 -52
  377. synth_ai/environments/environment/core.py +0 -67
  378. synth_ai/environments/environment/db/__init__.py +0 -1
  379. synth_ai/environments/environment/db/sqlite.py +0 -45
  380. synth_ai/environments/environment/registry.py +0 -233
  381. synth_ai/environments/environment/resources/sqlite.py +0 -45
  382. synth_ai/environments/environment/results.py +0 -1
  383. synth_ai/environments/environment/rewards/__init__.py +0 -1
  384. synth_ai/environments/environment/rewards/core.py +0 -29
  385. synth_ai/environments/environment/shared_engine.py +0 -26
  386. synth_ai/environments/environment/tools/__init__.py +0 -200
  387. synth_ai/environments/examples/__init__.py +0 -1
  388. synth_ai/environments/examples/bandit/__init__.py +0 -33
  389. synth_ai/environments/examples/bandit/engine.py +0 -294
  390. synth_ai/environments/examples/bandit/environment.py +0 -194
  391. synth_ai/environments/examples/bandit/taskset.py +0 -200
  392. synth_ai/environments/examples/crafter_classic/__init__.py +0 -8
  393. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +0 -250
  394. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +0 -59
  395. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +0 -152
  396. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +0 -24
  397. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +0 -1194
  398. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +0 -56
  399. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +0 -32
  400. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  401. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +0 -384
  402. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +0 -53
  403. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +0 -178
  404. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +0 -222
  405. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +0 -183
  406. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +0 -210
  407. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +0 -206
  408. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +0 -49
  409. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +0 -64
  410. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +0 -88
  411. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +0 -77
  412. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +0 -324
  413. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  414. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +0 -362
  415. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +0 -49
  416. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +0 -332
  417. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +0 -97
  418. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +0 -217
  419. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +0 -87
  420. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +0 -88
  421. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +0 -195
  422. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +0 -400
  423. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +0 -195
  424. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +0 -56
  425. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +0 -858
  426. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +0 -52
  427. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +0 -874
  428. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +0 -1412
  429. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +0 -216
  430. synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +0 -296
  431. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +0 -58
  432. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +0 -464
  433. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +0 -152
  434. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +0 -51
  435. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +0 -1412
  436. synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +0 -112
  437. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +0 -203
  438. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +0 -305
  439. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +0 -126
  440. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +0 -94
  441. synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +0 -142
  442. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +0 -26
  443. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +0 -984
  444. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +0 -724
  445. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +0 -386
  446. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +0 -205
  447. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +0 -150
  448. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +0 -283
  449. synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +0 -280
  450. synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +0 -456
  451. synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +0 -166
  452. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +0 -102
  453. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +0 -128
  454. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +0 -655
  455. synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +0 -202
  456. synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +0 -166
  457. synth_ai/environments/examples/crafter_classic/config_logging.py +0 -111
  458. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  459. synth_ai/environments/examples/crafter_classic/engine.py +0 -579
  460. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +0 -64
  461. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +0 -6
  462. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +0 -75
  463. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +0 -267
  464. synth_ai/environments/examples/crafter_classic/environment.py +0 -404
  465. synth_ai/environments/examples/crafter_classic/taskset.py +0 -233
  466. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +0 -228
  467. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +0 -299
  468. synth_ai/environments/examples/crafter_custom/__init__.py +0 -4
  469. synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +0 -1
  470. synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +0 -202
  471. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +0 -7
  472. synth_ai/environments/examples/crafter_custom/crafter/config.py +0 -182
  473. synth_ai/environments/examples/crafter_custom/crafter/constants.py +0 -8
  474. synth_ai/environments/examples/crafter_custom/crafter/engine.py +0 -269
  475. synth_ai/environments/examples/crafter_custom/crafter/env.py +0 -262
  476. synth_ai/environments/examples/crafter_custom/crafter/objects.py +0 -417
  477. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +0 -187
  478. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +0 -118
  479. synth_ai/environments/examples/crafter_custom/dataset_builder.py +0 -373
  480. synth_ai/environments/examples/crafter_custom/environment.py +0 -312
  481. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +0 -159
  482. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +0 -158
  483. synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +0 -71
  484. synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +0 -105
  485. synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +0 -119
  486. synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +0 -52
  487. synth_ai/environments/examples/crafter_custom/run_dataset.py +0 -305
  488. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +0 -156
  489. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +0 -281
  490. synth_ai/environments/examples/enron/art_helpers/types_enron.py +0 -25
  491. synth_ai/environments/examples/enron/engine.py +0 -295
  492. synth_ai/environments/examples/enron/environment.py +0 -166
  493. synth_ai/environments/examples/enron/taskset.py +0 -112
  494. synth_ai/environments/examples/enron/units/keyword_stats.py +0 -112
  495. synth_ai/environments/examples/minigrid/__init__.py +0 -48
  496. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +0 -1188
  497. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +0 -48
  498. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +0 -562
  499. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +0 -221
  500. synth_ai/environments/examples/minigrid/engine.py +0 -589
  501. synth_ai/environments/examples/minigrid/environment.py +0 -274
  502. synth_ai/environments/examples/minigrid/environment_mapping.py +0 -242
  503. synth_ai/environments/examples/minigrid/puzzle_loader.py +0 -417
  504. synth_ai/environments/examples/minigrid/taskset.py +0 -583
  505. synth_ai/environments/examples/nethack/__init__.py +0 -7
  506. synth_ai/environments/examples/nethack/achievements.py +0 -337
  507. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +0 -981
  508. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +0 -74
  509. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +0 -831
  510. synth_ai/environments/examples/nethack/engine.py +0 -739
  511. synth_ai/environments/examples/nethack/environment.py +0 -256
  512. synth_ai/environments/examples/nethack/helpers/__init__.py +0 -41
  513. synth_ai/environments/examples/nethack/helpers/action_mapping.py +0 -301
  514. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +0 -402
  515. synth_ai/environments/examples/nethack/helpers/observation_utils.py +0 -433
  516. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +0 -200
  517. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +0 -269
  518. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +0 -308
  519. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +0 -431
  520. synth_ai/environments/examples/nethack/taskset.py +0 -323
  521. synth_ai/environments/examples/red/__init__.py +0 -7
  522. synth_ai/environments/examples/red/agent_demos/__init__.py +0 -1
  523. synth_ai/environments/examples/red/config_logging.py +0 -110
  524. synth_ai/environments/examples/red/engine.py +0 -694
  525. synth_ai/environments/examples/red/engine_helpers/__init__.py +0 -1
  526. synth_ai/environments/examples/red/engine_helpers/memory_map.py +0 -28
  527. synth_ai/environments/examples/red/engine_helpers/reward_components.py +0 -276
  528. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +0 -142
  529. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +0 -57
  530. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +0 -284
  531. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +0 -150
  532. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +0 -138
  533. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +0 -57
  534. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +0 -331
  535. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +0 -121
  536. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +0 -559
  537. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +0 -313
  538. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +0 -148
  539. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +0 -247
  540. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +0 -368
  541. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +0 -140
  542. synth_ai/environments/examples/red/environment.py +0 -238
  543. synth_ai/environments/examples/red/taskset.py +0 -79
  544. synth_ai/environments/examples/red/units/__init__.py +0 -1
  545. synth_ai/environments/examples/sokoban/__init__.py +0 -1
  546. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +0 -899
  547. synth_ai/environments/examples/sokoban/engine.py +0 -678
  548. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +0 -1
  549. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +0 -657
  550. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +0 -18
  551. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +0 -3
  552. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +0 -131
  553. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +0 -370
  554. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +0 -332
  555. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +0 -306
  556. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +0 -67
  557. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +0 -115
  558. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +0 -123
  559. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +0 -394
  560. synth_ai/environments/examples/sokoban/environment.py +0 -229
  561. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +0 -440
  562. synth_ai/environments/examples/sokoban/puzzle_loader.py +0 -312
  563. synth_ai/environments/examples/sokoban/taskset.py +0 -428
  564. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  565. synth_ai/environments/examples/tictactoe/__init__.py +0 -1
  566. synth_ai/environments/examples/tictactoe/engine.py +0 -368
  567. synth_ai/environments/examples/tictactoe/environment.py +0 -240
  568. synth_ai/environments/examples/tictactoe/taskset.py +0 -215
  569. synth_ai/environments/examples/verilog/__init__.py +0 -10
  570. synth_ai/environments/examples/verilog/engine.py +0 -329
  571. synth_ai/environments/examples/verilog/environment.py +0 -350
  572. synth_ai/environments/examples/verilog/taskset.py +0 -420
  573. synth_ai/environments/examples/wordle/__init__.py +0 -29
  574. synth_ai/environments/examples/wordle/engine.py +0 -398
  575. synth_ai/environments/examples/wordle/environment.py +0 -159
  576. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +0 -75
  577. synth_ai/environments/examples/wordle/taskset.py +0 -230
  578. synth_ai/environments/reproducibility/core.py +0 -42
  579. synth_ai/environments/reproducibility/helpers.py +0 -0
  580. synth_ai/environments/reproducibility/tree.py +0 -364
  581. synth_ai/environments/service/app.py +0 -98
  582. synth_ai/environments/service/core_routes.py +0 -1020
  583. synth_ai/environments/service/external_registry.py +0 -56
  584. synth_ai/environments/service/registry.py +0 -9
  585. synth_ai/environments/stateful/__init__.py +0 -1
  586. synth_ai/environments/stateful/core.py +0 -163
  587. synth_ai/environments/stateful/engine.py +0 -21
  588. synth_ai/environments/stateful/state.py +0 -7
  589. synth_ai/environments/tasks/api.py +0 -19
  590. synth_ai/environments/tasks/core.py +0 -80
  591. synth_ai/environments/tasks/filters.py +0 -41
  592. synth_ai/environments/tasks/utils.py +0 -91
  593. synth_ai/environments/v0_observability/history.py +0 -3
  594. synth_ai/environments/v0_observability/log.py +0 -2
  595. synth_ai/evals/base.py +0 -15
  596. synth_ai/experimental/synth_oss.py +0 -446
  597. synth_ai/handshake.py +0 -63
  598. synth_ai/http.py +0 -26
  599. synth_ai/http_client.py +0 -104
  600. synth_ai/inference/client.py +0 -20
  601. synth_ai/install_sqld.sh +0 -40
  602. synth_ai/jobs/client.py +0 -246
  603. synth_ai/learning/__init__.py +0 -24
  604. synth_ai/learning/config.py +0 -43
  605. synth_ai/learning/filtering.py +0 -0
  606. synth_ai/learning/ft_client.py +0 -59
  607. synth_ai/learning/offline/dpo.py +0 -0
  608. synth_ai/learning/offline/providers.py +0 -7
  609. synth_ai/learning/offline/sft.py +0 -0
  610. synth_ai/learning/offline/shared.py +0 -0
  611. synth_ai/learning/online/grpo.py +0 -0
  612. synth_ai/learning/online/irft.py +0 -0
  613. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  614. synth_ai/learning/prompts/gepa.py +0 -0
  615. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  616. synth_ai/learning/prompts/mipro.py +0 -289
  617. synth_ai/learning/prompts/random_search.py +0 -246
  618. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  619. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  620. synth_ai/learning/sse.py +0 -58
  621. synth_ai/learning/validators.py +0 -48
  622. synth_ai/lm/__init__.py +0 -51
  623. synth_ai/lm/caching/constants.py +0 -6
  624. synth_ai/lm/caching/dbs.py +0 -0
  625. synth_ai/lm/caching/ephemeral.py +0 -102
  626. synth_ai/lm/caching/handler.py +0 -137
  627. synth_ai/lm/caching/initialize.py +0 -11
  628. synth_ai/lm/caching/persistent.py +0 -114
  629. synth_ai/lm/config.py +0 -110
  630. synth_ai/lm/constants.py +0 -32
  631. synth_ai/lm/core/__init__.py +0 -8
  632. synth_ai/lm/core/all.py +0 -73
  633. synth_ai/lm/core/exceptions.py +0 -7
  634. synth_ai/lm/core/main.py +0 -319
  635. synth_ai/lm/core/main_v3.py +0 -594
  636. synth_ai/lm/core/synth_models.py +0 -48
  637. synth_ai/lm/core/vendor_clients.py +0 -188
  638. synth_ai/lm/cost/__init__.py +0 -0
  639. synth_ai/lm/cost/monitor.py +0 -1
  640. synth_ai/lm/cost/statefulness.py +0 -1
  641. synth_ai/lm/injection.py +0 -80
  642. synth_ai/lm/overrides.py +0 -206
  643. synth_ai/lm/provider_support/__init__.py +0 -8
  644. synth_ai/lm/provider_support/anthropic.py +0 -972
  645. synth_ai/lm/provider_support/openai.py +0 -1139
  646. synth_ai/lm/provider_support/suppress_logging.py +0 -31
  647. synth_ai/lm/structured_outputs/__init__.py +0 -0
  648. synth_ai/lm/structured_outputs/handler.py +0 -440
  649. synth_ai/lm/structured_outputs/inject.py +0 -297
  650. synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
  651. synth_ai/lm/tools/__init__.py +0 -3
  652. synth_ai/lm/tools/base.py +0 -172
  653. synth_ai/lm/unified_interface.py +0 -202
  654. synth_ai/lm/vendors/__init__.py +0 -0
  655. synth_ai/lm/vendors/base.py +0 -81
  656. synth_ai/lm/vendors/core/__init__.py +0 -0
  657. synth_ai/lm/vendors/core/anthropic_api.py +0 -387
  658. synth_ai/lm/vendors/core/gemini_api.py +0 -292
  659. synth_ai/lm/vendors/core/mistral_api.py +0 -322
  660. synth_ai/lm/vendors/core/openai_api.py +0 -225
  661. synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
  662. synth_ai/lm/vendors/local/__init__.py +0 -0
  663. synth_ai/lm/vendors/local/ollama.py +0 -0
  664. synth_ai/lm/vendors/openai_standard.py +0 -780
  665. synth_ai/lm/vendors/openai_standard_responses.py +0 -256
  666. synth_ai/lm/vendors/retries.py +0 -22
  667. synth_ai/lm/vendors/supported/__init__.py +0 -0
  668. synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
  669. synth_ai/lm/vendors/supported/deepseek.py +0 -69
  670. synth_ai/lm/vendors/supported/grok.py +0 -75
  671. synth_ai/lm/vendors/supported/groq.py +0 -16
  672. synth_ai/lm/vendors/supported/ollama.py +0 -15
  673. synth_ai/lm/vendors/supported/openrouter.py +0 -74
  674. synth_ai/lm/vendors/supported/together.py +0 -11
  675. synth_ai/lm/vendors/synth_client.py +0 -808
  676. synth_ai/lm/warmup.py +0 -186
  677. synth_ai/rl/secrets.py +0 -19
  678. synth_ai/scripts/verify_rewards.py +0 -100
  679. synth_ai/task/__init__.py +0 -10
  680. synth_ai/task/contracts.py +0 -120
  681. synth_ai/task/health.py +0 -28
  682. synth_ai/task/validators.py +0 -12
  683. synth_ai/tracing/__init__.py +0 -30
  684. synth_ai/tracing_v1/__init__.py +0 -33
  685. synth_ai/tracing_v3/config.py +0 -84
  686. synth_ai/tracing_v3/storage/config.py +0 -62
  687. synth_ai/tracing_v3/turso/__init__.py +0 -25
  688. synth_ai/tracing_v3/turso/daemon.py +0 -144
  689. synth_ai/tracing_v3/turso/manager.py +0 -760
  690. synth_ai/v0/tracing/__init__.py +0 -0
  691. synth_ai/v0/tracing/abstractions.py +0 -224
  692. synth_ai/v0/tracing/base_client.py +0 -91
  693. synth_ai/v0/tracing/client_manager.py +0 -131
  694. synth_ai/v0/tracing/config.py +0 -142
  695. synth_ai/v0/tracing/context.py +0 -146
  696. synth_ai/v0/tracing/decorators.py +0 -682
  697. synth_ai/v0/tracing/events/__init__.py +0 -0
  698. synth_ai/v0/tracing/events/manage.py +0 -147
  699. synth_ai/v0/tracing/events/scope.py +0 -86
  700. synth_ai/v0/tracing/events/store.py +0 -228
  701. synth_ai/v0/tracing/immediate_client.py +0 -151
  702. synth_ai/v0/tracing/local.py +0 -18
  703. synth_ai/v0/tracing/log_client_base.py +0 -73
  704. synth_ai/v0/tracing/retry_queue.py +0 -186
  705. synth_ai/v0/tracing/trackers.py +0 -515
  706. synth_ai/v0/tracing/upload.py +0 -512
  707. synth_ai/v0/tracing/utils.py +0 -9
  708. synth_ai/v0/tracing_v1/__init__.py +0 -16
  709. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  710. synth_ai/v0/tracing_v1/base_client.py +0 -91
  711. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  712. synth_ai/v0/tracing_v1/config.py +0 -142
  713. synth_ai/v0/tracing_v1/context.py +0 -146
  714. synth_ai/v0/tracing_v1/decorators.py +0 -703
  715. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  716. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  717. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  718. synth_ai/v0/tracing_v1/events/store.py +0 -228
  719. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  720. synth_ai/v0/tracing_v1/local.py +0 -18
  721. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  722. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  723. synth_ai/v0/tracing_v1/trackers.py +0 -515
  724. synth_ai/v0/tracing_v1/upload.py +0 -527
  725. synth_ai/v0/tracing_v1/utils.py +0 -9
  726. synth_ai/zyk/__init__.py +0 -30
  727. synth_ai-0.2.8.dev2.dist-info/METADATA +0 -129
  728. synth_ai-0.2.8.dev2.dist-info/RECORD +0 -420
  729. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/__init__.py +0 -0
  730. /synth_ai/{lm/caching → core/apps}/__init__.py +0 -0
  731. /synth_ai/{tracing_v3 → core/tracing_v3}/lm_call_record_abstractions.py +0 -0
  732. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/__init__.py +0 -0
  733. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/exceptions.py +0 -0
  734. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/types.py +0 -0
  735. /synth_ai/{compound/cais.py → py.typed} +0 -0
  736. /synth_ai/{learning → sdk/learning}/core.py +0 -0
  737. /synth_ai/{learning → sdk/learning}/gateway.py +0 -0
  738. {synth_ai-0.2.8.dev2.dist-info → synth_ai-0.4.3.dist-info}/WHEEL +0 -0
  739. {synth_ai-0.2.8.dev2.dist-info → synth_ai-0.4.3.dist-info}/licenses/LICENSE +0 -0
  740. {synth_ai-0.2.8.dev2.dist-info → synth_ai-0.4.3.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,45 @@
1
1
  from __future__ import annotations
2
2
 
3
- import argparse
3
+ import contextlib
4
4
  import json
5
5
  import os
6
- import sys
7
- import time
8
- from pathlib import Path
9
- from typing import Any, Dict, Callable
10
6
  import shutil
11
7
  import stat
8
+ import sys
12
9
  import textwrap
10
+ import time
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from synth_ai.cli.demo_apps.demo_registry import (
16
+ DemoTemplate,
17
+ get_demo_template,
18
+ list_demo_templates,
19
+ )
20
+ from synth_ai.cli.demo_apps.demo_task_apps import core as demo_core
21
+ from synth_ai.cli.demo_apps.demo_task_apps.core import DEFAULT_TASK_APP_SECRET_NAME, DemoEnv
22
+ from synth_ai.core.process import get_subprocess_env, should_filter_log_line
23
+
24
+ try:
25
+ from synth_ai.handshake import HandshakeError, run_handshake # type: ignore[import-untyped]
26
+ except ImportError:
27
+ # handshake module may not exist in all environments
28
+ HandshakeError = Exception # type: ignore[assignment,misc]
29
+ def run_handshake() -> None: # type: ignore[misc]
30
+ pass
31
+
13
32
 
14
- from synth_ai.demos.demo_task_apps import core as demo_core
15
- from synth_ai.handshake import run_handshake, HandshakeError
16
- from synth_ai.demos.demo_task_apps.core import DemoEnv
33
+ def _key_preview(value: str, label: str) -> str:
34
+ """Return a short descriptor for a secret without leaking the full value."""
35
+ try:
36
+ text = value or ""
37
+ length = len(text)
38
+ prefix = text[:6] if length >= 6 else text
39
+ suffix = text[-5:] if length >= 5 else text
40
+ return f"{label} len={length} prefix={prefix} last5={suffix}"
41
+ except Exception:
42
+ return f"{label} len=0"
17
43
 
18
44
 
19
45
  def _is_modal_public_url(u: str) -> bool:
@@ -26,35 +52,75 @@ def _is_modal_public_url(u: str) -> bool:
26
52
  return False
27
53
 
28
54
 
29
- def cmd_setup(_args: argparse.Namespace) -> int:
30
- # 1) Always perform SDK handshake and overwrite .env with returned keys
55
+ def setup() -> int:
56
+ # Change to demo directory if stored
57
+ demo_dir = demo_core.load_demo_dir()
58
+ if demo_dir and os.path.isdir(demo_dir):
59
+ os.chdir(demo_dir)
60
+ print(f"Using demo directory: {demo_dir}")
61
+
62
+ # 1) Try to fetch keys from frontend; fall back to manual input if fetch fails
63
+ synth_key = ""
64
+ rl_env_key = ""
65
+ org_name = "this organization"
66
+
31
67
  try:
32
68
  print("\n⏳ Connecting SDK to your browser session…")
33
69
  res = run_handshake()
34
- user = res.get("user") or {}
35
- org = res.get("org") or {}
36
- keys = res.get("keys") or {}
37
- synth_key = str(keys.get("synth") or "").strip()
38
- rl_env_key = str(keys.get("rl_env") or "").strip()
39
- if not synth_key or not rl_env_key:
40
- raise HandshakeError("handshake returned missing keys")
41
- # Overwrite .env with the latest values from the account/org
42
- demo_core.persist_dotenv_values({
70
+ # Type narrowing for dict access - run_handshake returns dict[str, Any] | None
71
+ res_dict: dict[str, Any] = res if isinstance(res, dict) else {}
72
+ org_val = res_dict.get("org") # type: ignore[misc]
73
+ org: dict[str, Any] = org_val if isinstance(org_val, dict) else {}
74
+ keys_val = res_dict.get("keys") # type: ignore[misc]
75
+ keys: dict[str, Any] = keys_val if isinstance(keys_val, dict) else {}
76
+ synth_key = str(keys.get("synth") or "").strip() # type: ignore[misc]
77
+ rl_env_key = str(keys.get("rl_env") or "").strip() # type: ignore[misc]
78
+ org_name = org.get("name") or "this organization" # type: ignore[misc]
79
+ print(f"✅ Connected to {org_name}!")
80
+ except (HandshakeError, Exception) as e:
81
+ print(f"⚠️ Failed to fetch keys from frontend: {e}")
82
+ print("Falling back to manual entry...")
83
+
84
+ # Prompt for manual input if any key is missing
85
+ if not synth_key:
86
+ try:
87
+ synth_key = input(
88
+ "Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
89
+ ).strip()
90
+ except (EOFError, KeyboardInterrupt):
91
+ print("\nSetup cancelled.")
92
+ return 1
93
+ if not synth_key:
94
+ print("Synth API key is required.")
95
+ return 1
96
+
97
+ if not rl_env_key:
98
+ try:
99
+ rl_env_key = input(
100
+ "Failed to fetch your RL Environment API key. Please enter your RL Environment API key here:\n> "
101
+ ).strip()
102
+ except (EOFError, KeyboardInterrupt):
103
+ print("\nSetup cancelled.")
104
+ return 1
105
+ if not rl_env_key:
106
+ print("RL Environment API key is required.")
107
+ return 1
108
+
109
+ # Persist both keys to .env
110
+ dotenv_path = demo_core.persist_dotenv_values(
111
+ {
43
112
  "SYNTH_API_KEY": synth_key,
44
113
  "ENVIRONMENT_API_KEY": rl_env_key,
45
- })
46
- org_name = (org.get("name") or "this organization")
47
- print(f"✅ Connected to {org_name}!")
48
- except HandshakeError as e:
49
- print(f"Handshake failed: {e}")
50
- return 1
51
- except Exception as e:
52
- print(f"Unexpected handshake error: {e}")
53
- return 1
114
+ }
115
+ )
116
+
117
+ # Store .env path for subsequent commands
118
+ demo_core.persist_env_file_path(dotenv_path)
54
119
 
55
120
  # 2) Reload env after handshake to pick up values from .env (suppress env prints)
56
- import io
57
121
  import contextlib
122
+ import io
123
+
58
124
  _buf = io.StringIO()
59
125
  with contextlib.redirect_stdout(_buf):
60
126
  env = demo_core.load_env()
@@ -71,22 +137,22 @@ def cmd_setup(_args: argparse.Namespace) -> int:
71
137
  return
72
138
  current = env.task_app_base_url
73
139
  needs_lookup = False
74
- if not current:
75
- needs_lookup = True
76
- elif not _is_modal_public_url(current):
140
+ if not current or not _is_modal_public_url(current):
77
141
  needs_lookup = True
78
142
  if not needs_lookup:
79
143
  return
80
- code, out = _popen_capture([
81
- "uv",
82
- "run",
83
- "python",
84
- "-m",
85
- "modal",
86
- "app",
87
- "url",
88
- env.task_app_name,
89
- ])
144
+ code, out = _popen_capture(
145
+ [
146
+ "uv",
147
+ "run",
148
+ "python",
149
+ "-m",
150
+ "modal",
151
+ "app",
152
+ "url",
153
+ env.task_app_name,
154
+ ]
155
+ )
90
156
  if code != 0 or not out:
91
157
  return
92
158
  new_url = ""
@@ -100,7 +166,6 @@ def cmd_setup(_args: argparse.Namespace) -> int:
100
166
  dotenv_values = {
101
167
  "TASK_APP_BASE_URL": new_url,
102
168
  "TASK_APP_NAME": env.task_app_name,
103
- "TASK_APP_SECRET_NAME": env.task_app_secret_name or f"{env.task_app_name}-secret",
104
169
  }
105
170
  demo_core.persist_dotenv_values(dotenv_values)
106
171
  os.environ["TASK_APP_BASE_URL"] = new_url
@@ -108,7 +173,7 @@ def cmd_setup(_args: argparse.Namespace) -> int:
108
173
 
109
174
  # Keys have been written already via handshake; avoid any interactive prompts
110
175
  synth_key = env.synth_api_key.strip()
111
- if not local_env.get("SYNTH_API_KEY") and synth_key:
176
+ if not local_env.get("SYNTH_API_KEY") and synth_key: # type: ignore[misc]
112
177
  demo_core.persist_dotenv_values({"SYNTH_API_KEY": synth_key})
113
178
  _refresh_env()
114
179
 
@@ -117,15 +182,16 @@ def cmd_setup(_args: argparse.Namespace) -> int:
117
182
 
118
183
  _maybe_fix_task_url()
119
184
 
120
- ok_backend = False
121
- ok_task = False
122
185
  if env.dev_backend_url:
123
- api = env.dev_backend_url.rstrip("/") + ("" if env.dev_backend_url.endswith("/api") else "/api")
124
- ok_backend = demo_core.assert_http_ok(api + "/health", method="GET")
186
+ api = env.dev_backend_url.rstrip("/") + (
187
+ "" if env.dev_backend_url.endswith("/api") else "/api"
188
+ )
189
+ demo_core.assert_http_ok(api + "/health", method="GET")
125
190
  # Intentionally suppress backend health print for concise output
126
191
  if env.task_app_base_url:
127
- ok_task = demo_core.assert_http_ok(env.task_app_base_url.rstrip("/") + "/health", method="GET") or \
128
- demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
192
+ demo_core.assert_http_ok(
193
+ env.task_app_base_url.rstrip("/") + "/health", method="GET"
194
+ ) or demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
129
195
  # Intentionally suppress task app health print
130
196
  else:
131
197
  print("\nSet your task app URL by running:\nuvx synth-ai rl_demo deploy\n")
@@ -133,13 +199,19 @@ def cmd_setup(_args: argparse.Namespace) -> int:
133
199
  # Omit uv version print to keep output concise
134
200
 
135
201
  # Keep exit code neutral; not all checks are critical for pairing
202
+ print(f"\nKeys saved to: {dotenv_path}")
136
203
  return 0
137
204
 
138
205
 
139
- def _popen_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
206
+ def _popen_capture(
207
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
208
+ ) -> tuple[int, str]:
140
209
  import subprocess
210
+
141
211
  try:
142
- proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
212
+ proc = subprocess.Popen(
213
+ cmd, cwd=cwd, env=get_subprocess_env(env), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
214
+ )
143
215
  out, _ = proc.communicate()
144
216
  return int(proc.returncode or 0), out or ""
145
217
  except Exception as e:
@@ -156,7 +228,7 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
156
228
  proc = subprocess.Popen(
157
229
  cmd,
158
230
  cwd=cwd,
159
- env=env,
231
+ env=get_subprocess_env(env),
160
232
  stdout=subprocess.PIPE,
161
233
  stderr=subprocess.STDOUT,
162
234
  text=True,
@@ -169,7 +241,8 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
169
241
  def _pump(stdout) -> None:
170
242
  try:
171
243
  for line in stdout:
172
- print(line.rstrip())
244
+ if not should_filter_log_line(line):
245
+ print(line.rstrip())
173
246
  except Exception:
174
247
  pass
175
248
 
@@ -183,7 +256,9 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
183
256
  return int(proc.returncode or 0)
184
257
 
185
258
 
186
- def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
259
+ def _popen_stream_capture(
260
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
261
+ ) -> tuple[int, str]:
187
262
  """Stream subprocess output to stdout and also capture it into a buffer."""
188
263
  import subprocess
189
264
  import threading
@@ -193,7 +268,7 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
193
268
  proc = subprocess.Popen(
194
269
  cmd,
195
270
  cwd=cwd,
196
- env=env,
271
+ env=get_subprocess_env(env),
197
272
  stdout=subprocess.PIPE,
198
273
  stderr=subprocess.STDOUT,
199
274
  text=True,
@@ -207,8 +282,9 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
207
282
  try:
208
283
  for line in stdout:
209
284
  line = line.rstrip()
210
- print(line)
211
- buf_lines.append(line)
285
+ if not should_filter_log_line(line):
286
+ print(line)
287
+ buf_lines.append(line)
212
288
  except Exception:
213
289
  pass
214
290
 
@@ -222,55 +298,6 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
222
298
  return int(proc.returncode or 0), "\n".join(buf_lines)
223
299
 
224
300
 
225
- def _mask_secret_args(args: list[str]) -> list[str]:
226
- masked: list[str] = []
227
- for a in args:
228
- if "=" in a and any(a.startswith(prefix) for prefix in ("ENVIRONMENT_API_KEY=", "OPENAI_API_KEY=", "SYNTH_API_KEY=")):
229
- try:
230
- key, value = a.split("=", 1)
231
- tail = value[-5:] if len(value) >= 5 else value
232
- masked.append(f"{key}=***{tail}")
233
- except Exception:
234
- masked.append("<masked>")
235
- else:
236
- masked.append(a)
237
- return masked
238
-
239
-
240
- def _ensure_modal_secret(
241
- secret_name: str,
242
- *,
243
- values: dict[str, str],
244
- label: str = "deploy",
245
- replace: bool = False,
246
- ) -> bool:
247
- prefix = f"[{label}]"
248
- if not secret_name.strip():
249
- raise RuntimeError("Secret name is required")
250
-
251
- if not values:
252
- raise RuntimeError("No values provided to create Modal secret")
253
-
254
- create_args = [f"{k}={v}" for k, v in values.items()]
255
- create_cmd = ["uv", "run", "modal", "secret", "create", secret_name, *create_args]
256
-
257
- if replace:
258
- print(f"{prefix} Removing Modal secret '{secret_name}' (if present)…")
259
- delete_cmd = ["bash", "-lc", f"printf 'y\\n' | uv run modal secret delete {secret_name}"]
260
- print(f"{prefix} Command:", " ".join(delete_cmd))
261
- delete_code = _popen_stream(delete_cmd)
262
- if delete_code != 0:
263
- print(f"{prefix} Warning: delete command exited with {delete_code}; continuing to create")
264
-
265
- print(f"\n{prefix} Creating Modal secret '{secret_name}'…")
266
- print(f"{prefix} Command:", " ".join(_mask_secret_args(create_cmd)))
267
- code = _popen_stream(create_cmd)
268
- if code != 0:
269
- raise RuntimeError("Failed to provision Modal secret (see logs above)")
270
-
271
- return True
272
-
273
-
274
301
  def _fmt_float(value: float) -> str:
275
302
  return f"{value:.10g}"
276
303
 
@@ -283,11 +310,23 @@ def _find_asgi_apps(root: Path) -> list[Path]:
283
310
  - "@modal.asgi_app()"
284
311
  """
285
312
  results: list[Path] = []
286
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
313
+ skip_dirs = {
314
+ ".git",
315
+ ".hg",
316
+ ".svn",
317
+ "node_modules",
318
+ "dist",
319
+ "build",
320
+ "__pycache__",
321
+ ".ruff_cache",
322
+ ".mypy_cache",
323
+ "venv",
324
+ ".venv",
325
+ }
287
326
  for dirpath, dirnames, filenames in os.walk(root):
288
327
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
289
328
  for name in filenames:
290
- if not name.endswith(".py"):
329
+ if not str(name).endswith(".py"):
291
330
  continue
292
331
  path = Path(dirpath) / name
293
332
  try:
@@ -297,16 +336,20 @@ def _find_asgi_apps(root: Path) -> list[Path]:
297
336
  results.append(path)
298
337
  except Exception:
299
338
  continue
339
+
300
340
  # Stable order: prioritize files under synth_demo/ first, then alphabetical
301
341
  def _priority(p: Path) -> tuple[int, str]:
302
342
  rel = str(p.resolve())
303
343
  in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
304
344
  return (0 if in_demo else 1, rel)
345
+
305
346
  results.sort(key=_priority)
306
347
  return results
307
348
 
308
349
 
309
- def _prompt_value(label: str, default: str | int | float, cast: Callable[[str], Any] | None = None) -> Any:
350
+ def _prompt_value(
351
+ label: str, default: str | int | float, cast: Callable[[str], Any] | None = None
352
+ ) -> Any:
310
353
  prompt = f"{label} [{default}]: "
311
354
  try:
312
355
  raw = input(prompt).strip()
@@ -325,11 +368,23 @@ def _prompt_value(label: str, default: str | int | float, cast: Callable[[str],
325
368
 
326
369
  def _find_vllm_tomls(root: Path) -> list[Path]:
327
370
  results: list[Path] = []
328
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
371
+ skip_dirs = {
372
+ ".git",
373
+ ".hg",
374
+ ".svn",
375
+ "node_modules",
376
+ "dist",
377
+ "build",
378
+ "__pycache__",
379
+ ".ruff_cache",
380
+ ".mypy_cache",
381
+ "venv",
382
+ ".venv",
383
+ }
329
384
  for dirpath, dirnames, filenames in os.walk(root):
330
385
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
331
386
  for name in filenames:
332
- if not name.endswith(".toml"):
387
+ if not str(name).endswith(".toml"):
333
388
  continue
334
389
  path = Path(dirpath) / name
335
390
  try:
@@ -345,7 +400,9 @@ def _create_new_config(env: DemoEnv) -> str:
345
400
  default_path = os.path.join(os.getcwd(), "demo_config.toml")
346
401
  while True:
347
402
  try:
348
- destination = input(f"Path to save new config [{default_path}]: ").strip() or default_path
403
+ destination = (
404
+ input(f"Path to save new config [{default_path}]: ").strip() or default_path
405
+ )
349
406
  except Exception:
350
407
  destination = default_path
351
408
  destination = os.path.abspath(destination)
@@ -354,7 +411,9 @@ def _create_new_config(env: DemoEnv) -> str:
354
411
  continue
355
412
  if os.path.exists(destination):
356
413
  try:
357
- overwrite = input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
414
+ overwrite = (
415
+ input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
416
+ )
358
417
  except Exception:
359
418
  overwrite = "n"
360
419
  if not overwrite.startswith("y"):
@@ -366,7 +425,9 @@ def _create_new_config(env: DemoEnv) -> str:
366
425
  model_name = _prompt_value("Model name", "Qwen/Qwen3-0.6B")
367
426
  compute_gpu_type = _prompt_value("Compute GPU type", "H100")
368
427
  compute_gpu_count = _prompt_value("Compute GPU count", 4, int)
369
- topology_gpu_type = _prompt_value("Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}")
428
+ topology_gpu_type = _prompt_value(
429
+ "Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}"
430
+ )
370
431
  gpus_for_vllm = _prompt_value("Topology gpus_for_vllm", 2, int)
371
432
  gpus_for_training = _prompt_value("Topology gpus_for_training", 1, int)
372
433
  tensor_parallel = _prompt_value("Topology tensor_parallel", 2, int)
@@ -384,8 +445,9 @@ def _create_new_config(env: DemoEnv) -> str:
384
445
  task_url_default = env.task_app_base_url or ""
385
446
  services_task_url = _prompt_value("services.task_url", task_url_default)
386
447
 
387
- template = textwrap.dedent(
388
- f"""\
448
+ template = (
449
+ textwrap.dedent(
450
+ f"""\
389
451
  # Crafter online RL training configuration (research local copy)
390
452
 
391
453
  [model]
@@ -527,7 +589,9 @@ def _create_new_config(env: DemoEnv) -> str:
527
589
  [services]
528
590
  task_url = \"{services_task_url}\"
529
591
  """
530
- ).strip() + "\n"
592
+ ).strip()
593
+ + "\n"
594
+ )
531
595
 
532
596
  with open(destination, "w", encoding="utf-8") as fh:
533
597
  fh.write(template)
@@ -546,7 +610,11 @@ def _select_or_create_config(explicit: str | None, env: DemoEnv) -> str:
546
610
  discovered = _find_vllm_tomls(search_root)
547
611
 
548
612
  extras: list[Path] = []
549
- packaged = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")))
613
+ packaged = Path(
614
+ os.path.abspath(
615
+ os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")
616
+ )
617
+ )
550
618
  extras.append(packaged)
551
619
  home_cfg = Path(os.path.expanduser("~/.synth-ai/demo_config.toml"))
552
620
  extras.append(home_cfg)
@@ -592,29 +660,36 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
592
660
 
593
661
  env_key = (env.env_api_key or "").strip()
594
662
  if not env_key:
595
- raise RuntimeError(f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first.")
663
+ raise RuntimeError(
664
+ f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first."
665
+ )
596
666
 
597
667
  task_url = env.task_app_base_url
598
668
  if not task_url or not _is_modal_public_url(task_url):
599
669
  resolved = ""
600
670
  if env.task_app_name:
601
671
  try:
602
- choice = input(
603
- f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
604
- ).strip().lower() or "y"
672
+ choice = (
673
+ input(f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: ")
674
+ .strip()
675
+ .lower()
676
+ or "y"
677
+ )
605
678
  except Exception:
606
679
  choice = "y"
607
680
  if choice.startswith("y"):
608
- code, out = _popen_capture([
609
- "uv",
610
- "run",
611
- "python",
612
- "-m",
613
- "modal",
614
- "app",
615
- "url",
616
- env.task_app_name,
617
- ])
681
+ code, out = _popen_capture(
682
+ [
683
+ "uv",
684
+ "run",
685
+ "python",
686
+ "-m",
687
+ "modal",
688
+ "app",
689
+ "url",
690
+ env.task_app_name,
691
+ ]
692
+ )
618
693
  if code == 0 and out:
619
694
  for tok in out.split():
620
695
  if _is_modal_public_url(tok):
@@ -623,7 +698,9 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
623
698
  if not resolved:
624
699
  print(f"[{label}] Task app URL not configured or not a valid Modal public URL.")
625
700
  print("Examples: https://<app-name>-fastapi-app.modal.run")
626
- entered = input("Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: ").strip()
701
+ entered = input(
702
+ "Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
703
+ ).strip()
627
704
  if not entered or not _is_modal_public_url(entered):
628
705
  raise RuntimeError(f"[{label}] Valid Task App URL is required.")
629
706
  task_url = entered.rstrip("/")
@@ -639,30 +716,26 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
639
716
  app_name = fallback
640
717
  demo_core.persist_task_url(task_url, name=app_name)
641
718
 
642
- secret_name = env.task_app_secret_name.strip() or f"{app_name}-secret"
643
719
  demo_core.persist_task_url(task_url, name=app_name)
644
- demo_core.persist_dotenv_values({
645
- "TASK_APP_BASE_URL": task_url,
646
- "TASK_APP_NAME": app_name,
647
- "TASK_APP_SECRET_NAME": secret_name,
648
- })
649
-
650
- openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
651
- secret_values: dict[str, str] = {"ENVIRONMENT_API_KEY": env_key}
652
- if openai_key:
653
- secret_values["OPENAI_API_KEY"] = openai_key
720
+ demo_core.persist_dotenv_values(
721
+ {
722
+ "TASK_APP_BASE_URL": task_url,
723
+ "TASK_APP_NAME": app_name,
724
+ "TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
725
+ }
726
+ )
727
+
654
728
  if synth_key:
655
- secret_values["SYNTH_API_KEY"] = synth_key
729
+ os.environ["SYNTH_API_KEY"] = synth_key
656
730
 
657
- _ensure_modal_secret(secret_name, values=secret_values, label=label, replace=True)
731
+ openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip() # type: ignore[misc]
732
+ if openai_key:
733
+ os.environ["OPENAI_API_KEY"] = openai_key
658
734
 
659
- rollout_url = task_url.rstrip("/") + "/health/rollout"
660
735
  print(f"[{label}] Verifying rollout health:")
661
736
  try:
662
737
  ek = (env_key or "").strip()
663
- ek_len = len(ek)
664
- ek_tail = ek[-5:] if ek_len >= 5 else ek
665
- print(f"[{label}] Using ENVIRONMENT_API_KEY len={ek_len} last5={ek_tail}")
738
+ print(f"[{label}] {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
666
739
  except Exception:
667
740
  pass
668
741
  health_base = task_url.rstrip("/")
@@ -673,7 +746,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
673
746
  print(f"[{label}] GET", h)
674
747
  rc, body = _http("GET", h, headers={"X-API-Key": env_key})
675
748
  if rc == 200:
676
- rollout_url = h
677
749
  break
678
750
  print(f"[{label}] status: {rc}")
679
751
  try:
@@ -685,41 +757,64 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
685
757
  print(f"[{label}] body:", preview)
686
758
  if rc != 200:
687
759
  print(f"[{label}] Warning: rollout health check failed ({rc}). Response: {body}")
760
+ with contextlib.suppress(Exception):
761
+ print(f"[{label}] Sent header X-API-Key → {_key_preview(env_key, 'X-API-Key')}")
688
762
  else:
689
763
  print(f"[{label}] Task app rollout health check OK.")
690
764
 
691
765
  os.environ["TASK_APP_BASE_URL"] = task_url
692
766
  os.environ["ENVIRONMENT_API_KEY"] = env_key
767
+ os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
693
768
  updated_env = demo_core.load_env()
694
769
  updated_env.env_api_key = env_key
695
770
  updated_env.task_app_base_url = task_url
696
771
  updated_env.task_app_name = app_name
697
- updated_env.task_app_secret_name = secret_name
772
+ updated_env.task_app_secret_name = DEFAULT_TASK_APP_SECRET_NAME
698
773
  return updated_env
699
774
 
700
775
 
701
- def cmd_deploy(args: argparse.Namespace) -> int:
776
+ def deploy(
777
+ local: bool = False, app: str | None = None, name: str | None = None, script: str | None = None
778
+ ) -> int:
779
+ # Change to demo directory if stored
780
+ demo_dir = demo_core.load_demo_dir()
781
+ if demo_dir and os.path.isdir(demo_dir):
782
+ os.chdir(demo_dir)
783
+ print(f"Using demo directory: {demo_dir}")
784
+
702
785
  env = demo_core.load_env()
786
+ os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
703
787
  cwd_env_path = os.path.join(os.getcwd(), ".env")
704
788
  local_env = demo_core.load_dotenv_file(cwd_env_path)
705
789
  url = ""
706
790
  app_name = env.task_app_name or ""
707
791
  try:
708
- if args.local:
792
+ if local:
709
793
  print("Starting local Task App…")
710
794
  import subprocess
711
- subprocess.Popen([sys.executable, "-c", "from synth_ai.demos.demo_task_apps.math.app import run; run()"],
712
- stdout=sys.stdout, stderr=sys.stderr)
795
+
796
+ subprocess.Popen(
797
+ [
798
+ sys.executable,
799
+ "-c",
800
+ "from synth_ai.cli.demo_apps.demo_task_apps.math.app import run; run()",
801
+ ],
802
+ env=get_subprocess_env(),
803
+ stdout=sys.stdout,
804
+ stderr=sys.stderr,
805
+ )
713
806
  target = "http://127.0.0.1:8080"
714
807
  app_name = ""
715
808
  for _ in range(30):
716
- if demo_core.assert_http_ok(target + "/health", method="GET") or demo_core.assert_http_ok(target, method="GET"):
809
+ if demo_core.assert_http_ok(
810
+ target + "/health", method="GET"
811
+ ) or demo_core.assert_http_ok(target, method="GET"):
717
812
  url = target
718
813
  break
719
814
  time.sleep(1)
720
815
  else:
721
816
  # Auto-detect app path if not supplied; prompt interactively from discovered ASGI apps
722
- app_path = os.path.abspath(args.app) if args.app else None
817
+ app_path = os.path.abspath(app) if app else None
723
818
  if not app_path or not os.path.isfile(app_path):
724
819
  # First pass: look for known common filenames
725
820
  candidates = [
@@ -738,7 +833,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
738
833
  rel = os.path.relpath(str(pth), os.getcwd())
739
834
  print(f" [{idx}] {rel}")
740
835
  try:
741
- sel = input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
836
+ sel = (
837
+ input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
838
+ )
742
839
  except Exception:
743
840
  sel = "1"
744
841
  try:
@@ -747,12 +844,15 @@ def cmd_deploy(args: argparse.Namespace) -> int:
747
844
  choice = 1
748
845
  choice = max(1, min(choice, len(found)))
749
846
  app_path = str(found[choice - 1].resolve())
750
- if not app_path and args.script:
847
+ if not app_path and script:
751
848
  # Legacy script fallback if user supplied --script explicitly
752
- from synth_ai.demos.demo_task_apps.math.deploy_modal import deploy as modal_deploy
753
- url = modal_deploy(script_path=args.script, env_api_key=env.env_api_key)
754
- if args.name:
755
- app_name = args.name
849
+ from synth_ai.cli.demo_apps.demo_task_apps.math.deploy_modal import (
850
+ deploy as modal_deploy,
851
+ )
852
+
853
+ url = modal_deploy(script_path=script, env_api_key=env.env_api_key)
854
+ if name:
855
+ app_name = name
756
856
  else:
757
857
  if not app_path:
758
858
  entered = input("Path to Modal app.py (e.g., ./task_app.py): ").strip()
@@ -763,7 +863,10 @@ def cmd_deploy(args: argparse.Namespace) -> int:
763
863
  raise FileNotFoundError(f"App file not found: {app_path}")
764
864
  # Surface the app path before asking for the name
765
865
  print(f"Using task app: {app_path}")
766
- suggested_name = args.name or f"synth-{os.path.splitext(os.path.basename(app_path))[0]}"
866
+ existing_name = (name or env.task_app_name or "").strip()
867
+ if not existing_name:
868
+ existing_name = f"synth-{os.path.splitext(os.path.basename(app_path))[0]}"
869
+ suggested_name = existing_name
767
870
  name_in = input(f"Modal app name [{suggested_name}]: ").strip() or suggested_name
768
871
  app_name = name_in
769
872
  print("\nAbout to deploy with:")
@@ -774,21 +877,23 @@ def cmd_deploy(args: argparse.Namespace) -> int:
774
877
  print("Aborted by user.")
775
878
  return 1
776
879
 
777
- secret_name = (env.task_app_secret_name or "").strip() or f"{name_in}-secret"
778
880
  existing_env_key = (env.env_api_key or "").strip()
779
881
  env_key: str | None = existing_env_key or None
780
882
  if existing_env_key:
781
883
  try:
782
- reuse_choice = input(
783
- "Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: "
784
- ).strip().lower() or "y"
884
+ reuse_choice = (
885
+ input("Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: ")
886
+ .strip()
887
+ .lower()
888
+ or "y"
889
+ )
785
890
  except Exception:
786
891
  reuse_choice = "y"
787
892
  if not reuse_choice.startswith("y"):
788
893
  env_key = None
789
894
 
790
895
  if env_key is None:
791
- from synth_ai.rl.secrets import mint_environment_api_key
896
+ from synth_ai.sdk.learning.rl.secrets import mint_environment_api_key
792
897
 
793
898
  env_key = mint_environment_api_key()
794
899
  demo_core.persist_env_api_key(env_key)
@@ -797,69 +902,90 @@ def cmd_deploy(args: argparse.Namespace) -> int:
797
902
  env.env_api_key = env_key
798
903
  local_env["ENVIRONMENT_API_KEY"] = env_key
799
904
  print("[deploy] Minted new ENVIRONMENT_API_KEY")
800
-
905
+ elif env_key:
906
+ os.environ["ENVIRONMENT_API_KEY"] = env_key
907
+
801
908
  # Optionally upload the new key to the backend using sealed box helper
802
- backend_base = env.dev_backend_url or ""
803
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
909
+ backend_base = (env.dev_backend_url or "").rstrip("/")
910
+ synth_key = (
911
+ env.synth_api_key
912
+ or os.environ.get("SYNTH_API_KEY") # type: ignore[misc]
913
+ or local_env.get("SYNTH_API_KEY") # type: ignore[misc]
914
+ or ""
915
+ ).strip()
804
916
  if backend_base and synth_key:
805
- backend_base = backend_base.rstrip("/")
806
- if not backend_base.endswith("/api"):
807
- backend_base = f"{backend_base}/api"
917
+ # Pass a base WITHOUT trailing /api to setup_environment_api_key,
918
+ # since it appends /api/v1/... internally.
919
+ non_api_base = (
920
+ backend_base[:-4] if backend_base.endswith("/api") else backend_base
921
+ )
922
+ try:
923
+ choice = (
924
+ input(f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: ")
925
+ .strip()
926
+ .lower()
927
+ or "y"
928
+ )
929
+ except Exception:
930
+ choice = "y"
931
+ if choice.startswith("y"):
808
932
  try:
809
- choice = input(
810
- f"Upload ENVIRONMENT_API_KEY to backend {backend_base}? [Y/n]: "
811
- ).strip().lower() or "y"
812
- except Exception:
813
- choice = "y"
814
- if choice.startswith("y"):
815
- try:
816
- print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {backend_base} …")
817
- from synth_ai.rl.env_keys import setup_environment_api_key
818
-
819
- setup_environment_api_key(backend_base.rstrip("/"), synth_key, token=env_key)
820
- print("[deploy] Backend sealed-box upload complete.")
821
- except Exception as upload_err:
822
- print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
823
- print(
824
- "Hint: run `uvx python -c \"from synth_ai.rl.env_keys import setup_environment_api_key as s;"
825
- " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
826
- )
827
-
828
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
933
+ print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
934
+ from synth_ai.sdk.learning.rl.env_keys import setup_environment_api_key
935
+
936
+ setup_environment_api_key(non_api_base, synth_key, token=env_key)
937
+ print("[deploy] Backend sealed-box upload complete.")
938
+ except Exception as upload_err:
939
+ print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
940
+ print(
941
+ 'Hint: run `uvx python -c "from synth_ai.sdk.learning.rl.env_keys import setup_environment_api_key as s;'
942
+ " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
943
+ )
944
+
945
+ synth_key = (
946
+ env.synth_api_key
947
+ or os.environ.get("SYNTH_API_KEY") # type: ignore[misc]
948
+ or local_env.get("SYNTH_API_KEY") # type: ignore[misc]
949
+ or ""
950
+ ).strip()
829
951
  if not synth_key:
830
- synth_key = input("Enter SYNTH_API_KEY for Modal secret (required): ").strip()
952
+ synth_key = input("Enter SYNTH_API_KEY for deployment (required): ").strip()
831
953
  if not synth_key:
832
- print("SYNTH_API_KEY is required to create the Modal secret.")
954
+ print("SYNTH_API_KEY is required for deployment.")
833
955
  return 1
834
956
  demo_core.persist_api_key(synth_key)
835
957
  demo_core.persist_dotenv_values({"SYNTH_API_KEY": synth_key})
836
958
  env.synth_api_key = synth_key
959
+ os.environ["SYNTH_API_KEY"] = synth_key
837
960
 
838
- openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
961
+ openai_key = (
962
+ os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or ""
963
+ ).strip()
839
964
  if not openai_key:
840
965
  openai_key = input(
841
966
  "Enter your OpenAI API key, found at https://platform.openai.com/api-keys\n> "
842
967
  ).strip()
843
968
  if not openai_key:
844
- print("OPENAI_API_KEY is required to create the Modal secret.")
969
+ print("OPENAI_API_KEY is required for deployment.")
845
970
  return 1
846
971
  demo_core.persist_dotenv_values({"OPENAI_API_KEY": openai_key})
847
972
  local_env["OPENAI_API_KEY"] = openai_key
973
+ os.environ["OPENAI_API_KEY"] = openai_key
848
974
 
849
- values = {"SYNTH_API_KEY": synth_key, "OPENAI_API_KEY": openai_key}
850
- if env_key:
851
- values["ENVIRONMENT_API_KEY"] = env_key
852
-
853
- try:
854
- created = _ensure_modal_secret(secret_name, values=values, label="deploy", replace=True)
855
- except RuntimeError as secret_err:
856
- print(f"Failed to prepare Modal secret '{secret_name}': {secret_err}")
857
- return 2
858
- if created:
859
- print(f"[deploy] Modal secret '{secret_name}' provisioned.")
860
-
861
- deploy_cmd = ["uv", "run", "python", "-m", "modal", "deploy", "--name", name_in, app_path]
862
- print("\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n")
975
+ deploy_cmd = [
976
+ "uv",
977
+ "run",
978
+ "python",
979
+ "-m",
980
+ "modal",
981
+ "deploy",
982
+ "--name",
983
+ name_in,
984
+ app_path,
985
+ ]
986
+ print(
987
+ "\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n"
988
+ )
863
989
  code, deploy_logs = _popen_stream_capture(deploy_cmd)
864
990
  if code != 0:
865
991
  raise RuntimeError(f"modal deploy failed (exit {code})")
@@ -867,6 +993,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
867
993
  if not url:
868
994
  try:
869
995
  import re as _re
996
+
870
997
  m_all = _re.findall(r"https?://[^\s]+\.modal\.run", deploy_logs or "")
871
998
  if m_all:
872
999
  url = m_all[-1].strip().rstrip("/")
@@ -881,7 +1008,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
881
1008
  break
882
1009
  # Fallback: try reading recent Modal logs for the app to find a URL line
883
1010
  if not url:
884
- code3, out3 = _popen_capture(["uv", "run", "python", "-m", "modal", "app", "list"])
1011
+ code3, out3 = _popen_capture(
1012
+ ["uv", "run", "python", "-m", "modal", "app", "list"]
1013
+ )
885
1014
  if code3 == 0 and out3:
886
1015
  for line in out3.splitlines():
887
1016
  if name_in in line:
@@ -894,7 +1023,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
894
1023
  # Prompt user if still no valid URL
895
1024
  if not url:
896
1025
  print("\nCould not auto-detect a public Modal URL for the app.")
897
- entered = input("Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: ").strip()
1026
+ entered = input(
1027
+ "Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: "
1028
+ ).strip()
898
1029
  if entered and _is_modal_public_url(entered):
899
1030
  url = entered.rstrip("/")
900
1031
  if not url:
@@ -906,7 +1037,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
906
1037
  dotenv_values = {"TASK_APP_BASE_URL": url}
907
1038
  if app_name:
908
1039
  dotenv_values["TASK_APP_NAME"] = app_name
909
- dotenv_values["TASK_APP_SECRET_NAME"] = f"{app_name}-secret"
1040
+ dotenv_values["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
910
1041
  dotenv_path = demo_core.persist_dotenv_values(dotenv_values)
911
1042
  print(f"TASK_APP_BASE_URL={url}")
912
1043
  if app_name:
@@ -915,16 +1046,16 @@ def cmd_deploy(args: argparse.Namespace) -> int:
915
1046
  print(f" export TASK_APP_BASE_URL={url}")
916
1047
  if app_name:
917
1048
  print(f" export TASK_APP_NAME={app_name}")
918
- print(f" export TASK_APP_SECRET_NAME={app_name}-secret")
919
1049
  print(f"Persisted to {dotenv_path}")
920
- print("Next: uvx synth-ai run")
1050
+ print("\nNext step:\n$ uvx synth-ai run")
921
1051
  return 0
922
1052
  except Exception as e:
923
1053
  print(f"Deploy error: {e}")
924
1054
  return 2
925
1055
 
926
-
927
- print("`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches.")
1056
+ print(
1057
+ "`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches."
1058
+ )
928
1059
  env = demo_core.load_env()
929
1060
  synth_key = (env.synth_api_key or "").strip()
930
1061
  if not synth_key:
@@ -956,133 +1087,314 @@ def cmd_deploy(args: argparse.Namespace) -> int:
956
1087
  return 0
957
1088
 
958
1089
 
959
- def cmd_init(args: argparse.Namespace) -> int:
960
- """Initialize a Modal-ready Math Task App in the current directory.
1090
+ def _ensure_modal_installed() -> None:
1091
+ """Install the modal package if it is not already available and check authentication."""
961
1092
 
962
- Copies `examples/rl/task_app.py` and `examples/rl/deploy_task_app.sh` into CWD.
963
- Creates a `.env` with placeholders if it does not exist.
964
- """
1093
+ # Check if modal is installed
1094
+ modal_installed = False
965
1095
  try:
966
- # Ensure `modal` is installed for deployment flows
967
- def _has_modal() -> bool:
968
- try:
969
- import importlib.util as _iu
970
- return _iu.find_spec("modal") is not None
971
- except Exception:
972
- return False
1096
+ import importlib.util as _iu
973
1097
 
974
- if not _has_modal():
975
- print("modal not found; installing…")
976
- # Prefer uv if available; otherwise fallback to pip
977
- try:
978
- if shutil.which("uv"):
979
- code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
980
- else:
981
- code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
982
- if code != 0:
983
- print(out)
984
- print("Failed to install modal; continuing may fail.")
985
- else:
986
- print("modal installed successfully.")
987
- except Exception as e:
988
- print(f"modal install error: {e}")
989
- # Re-check
990
- if not _has_modal():
991
- print("Warning: modal is still not importable after install attempt.")
992
- else:
993
- print("modal found")
994
-
995
- here = os.getcwd()
996
- demo_dir = os.path.join(here, "synth_demo")
997
- os.makedirs(demo_dir, exist_ok=True)
998
- # Paths inside synth_demo/
999
- dst_task_py = os.path.join(demo_dir, "task_app.py")
1000
- dst_deploy = os.path.join(demo_dir, "deploy_task_app.sh")
1001
- env_path = os.path.join(demo_dir, ".env")
1002
- dst_cfg = os.path.join(demo_dir, "demo_config.toml")
1003
-
1004
- # Copy packaged math modal task app into synth_demo/task_app.py
1005
- src_modal = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "modal_task_app.py"))
1006
- if not os.path.isfile(src_modal):
1007
- print("Init failed: packaged math modal task app not found.")
1008
- print(f"Looked for: {src_modal}")
1009
- return 1
1010
- if os.path.exists(dst_task_py) and not getattr(args, "force", False):
1011
- print(f"Refusing to overwrite existing file: {dst_task_py} (use --force)")
1012
- return 1
1013
- shutil.copy2(src_modal, dst_task_py)
1014
-
1015
- # Create deploy script in synth_demo/
1016
- deploy_text = r"""#!/usr/bin/env bash
1017
- set -euo pipefail
1018
-
1019
- HERE=$(cd "$(dirname "$0")" && pwd)
1020
- APP="$HERE/task_app.py"
1021
- if [ -f "$HERE/.env" ]; then
1022
- # shellcheck disable=SC2046
1023
- export $(grep -v '^#' "$HERE/.env" | xargs -I{} echo {})
1024
- fi
1025
- uv run modal deploy "$APP" | tee "$HERE/.last_deploy.log"
1026
- URL=$(grep -Eo 'https://[^ ]+\.modal\.run' "$HERE/.last_deploy.log" | tail -1 || true)
1027
- if [ -n "$URL" ]; then
1028
- if grep -q '^TASK_APP_BASE_URL=' "$HERE/.env" 2>/dev/null; then
1029
- sed -i.bak "s#^TASK_APP_BASE_URL=.*#TASK_APP_BASE_URL=$URL#" "$HERE/.env" || true
1030
- else
1031
- echo "TASK_APP_BASE_URL=$URL" >> "$HERE/.env"
1032
- fi
1033
- echo "Saved TASK_APP_BASE_URL to $HERE/.env"
1034
- fi
1035
- """
1036
- _write_text(dst_deploy, deploy_text)
1098
+ if _iu.find_spec("modal") is not None:
1099
+ modal_installed = True
1100
+ except Exception:
1101
+ pass
1102
+
1103
+ # Install modal if needed
1104
+ if not modal_installed:
1105
+ print("modal not found; installing…")
1106
+ try:
1107
+ if shutil.which("uv"):
1108
+ code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
1109
+ else:
1110
+ code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
1111
+ if code != 0:
1112
+ print(out)
1113
+ print("Failed to install modal; continuing may fail.")
1114
+ return
1115
+ else:
1116
+ print("✓ modal installed successfully")
1117
+ modal_installed = True
1118
+ except Exception as exc:
1119
+ print(f"modal install error: {exc}")
1120
+ return
1121
+
1122
+ # Verify modal is importable
1123
+ if modal_installed:
1037
1124
  try:
1038
- st = os.stat(dst_deploy)
1039
- os.chmod(dst_deploy, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1125
+ import importlib.util as _iu
1126
+
1127
+ if _iu.find_spec("modal") is None:
1128
+ print("Warning: modal is still not importable after install attempt.")
1129
+ return
1040
1130
  except Exception:
1041
- pass
1131
+ print("Warning: unable to verify modal installation.")
1132
+ return
1133
+
1134
+ # Check modal authentication status
1135
+ auth_ok, auth_msg = demo_core.modal_auth_status()
1136
+ if auth_ok:
1137
+ print(f"✓ Modal authenticated: {auth_msg}")
1138
+ else:
1139
+ print("\n⚠️ Modal authentication required")
1140
+ print(f" Status: {auth_msg}")
1141
+ print("\n To authenticate Modal, run:")
1142
+ print(" modal setup")
1143
+ print("\n Or set environment variables:")
1144
+ print(" export MODAL_TOKEN_ID=your-token-id")
1145
+ print(" export MODAL_TOKEN_SECRET=your-token-secret")
1146
+ print("\n You can deploy later after authenticating.\n")
1147
+
1148
+
1149
+ def init(template: str | None = None, dest: str | None = None, force: bool = False) -> int:
1150
+ """Materialise a demo task app template into the current directory."""
1151
+
1152
+ templates = list(list_demo_templates())
1153
+ if not templates:
1154
+ print("No demo templates registered. Update synth_ai/demo_registry.py to add entries.")
1155
+ return 1
1156
+
1157
+ selected: DemoTemplate | None = None
1158
+ if template:
1159
+ selected = get_demo_template(template)
1160
+ if selected is None:
1161
+ available = ", ".join(t.template_id for t in templates)
1162
+ print(f"Unknown template '{template}'. Available: {available}")
1163
+ return 1
1164
+ else:
1165
+ if force:
1166
+ selected = templates[0]
1167
+ print(
1168
+ f"Using default template: {selected.name} ({selected.template_id}) "
1169
+ f"(pass --template to choose another)"
1170
+ )
1171
+ else:
1172
+ print("Select a demo template:" + "\n")
1173
+ for idx, tpl in enumerate(templates, start=1):
1174
+ print(f" [{idx}] {tpl.name} ({tpl.template_id})")
1175
+ print(f" {tpl.description}")
1176
+ try:
1177
+ choice_raw = input(f"Enter choice [1-{len(templates)}] (default 1): ").strip() or "1"
1178
+ except Exception:
1179
+ choice_raw = "1"
1180
+ if not choice_raw.isdigit():
1181
+ print("Selection must be a number.")
1182
+ return 1
1183
+ choice_idx = int(choice_raw)
1184
+ if not 1 <= choice_idx <= len(templates):
1185
+ print("Selection out of range.")
1186
+ return 1
1187
+ selected = templates[choice_idx - 1]
1188
+
1189
+ assert selected is not None
1190
+
1191
+ default_subdir = selected.default_subdir or selected.template_id
1192
+
1193
+ # Check if default destination is already occupied and switch to local_demos/ if needed
1194
+ if dest:
1195
+ default_dest = Path(dest).expanduser().resolve()
1196
+ else:
1197
+ primary_dest = Path.cwd() / default_subdir
1198
+ if primary_dest.exists() and any(primary_dest.iterdir()):
1199
+ # Switch to local_demos/ automatically if primary location is occupied
1200
+ default_dest = (Path.cwd() / "local_demos" / default_subdir).resolve()
1201
+ else:
1202
+ default_dest = primary_dest.resolve()
1042
1203
 
1043
- # Seed .env if not present
1044
- if not os.path.exists(env_path):
1045
- _write_text(env_path, "\n".join([
1046
- "# Required for task app auth to environment service",
1047
- "ENVIRONMENT_API_KEY=",
1048
- "",
1049
- "# Optional: for CLI job submission and proxying OpenAI models",
1050
- "SYNTH_API_KEY=",
1051
- "OPENAI_API_KEY=",
1052
- "",
1053
- "# Optional: set to 'prod' to use production names",
1054
- "ENVIRONMENT=",
1055
- ]) + "\n")
1056
-
1057
- # Seed demo_config.toml from packaged default if not present (or overwrite with --force)
1058
- packaged_cfg = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml"))
1204
+ if force:
1205
+ dest_input = ""
1206
+ else:
1059
1207
  try:
1060
- if os.path.isfile(packaged_cfg):
1061
- if not os.path.exists(dst_cfg) or getattr(args, "force", False):
1062
- shutil.copy2(packaged_cfg, dst_cfg)
1208
+ dest_input = input(f"Destination directory [{default_dest}]: ").strip()
1063
1209
  except Exception:
1064
- pass
1210
+ dest_input = ""
1211
+ destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
1212
+
1213
+ # Track whether we should skip individual file prompts (if we already cleared the directory)
1214
+ directory_cleared = False
1215
+
1216
+ if destination.exists():
1217
+ if destination.is_file():
1218
+ print(f"Destination {destination} is a file. Provide a directory path.")
1219
+ return 1
1220
+ if any(destination.iterdir()):
1221
+ if force:
1222
+ response = "y"
1223
+ else:
1224
+ try:
1225
+ response = (
1226
+ input(f"Destination {destination} is not empty. Overwrite? [y/N]: ")
1227
+ .strip()
1228
+ .lower()
1229
+ )
1230
+ except (EOFError, KeyboardInterrupt):
1231
+ print("\nCancelled.")
1232
+ return 1
1233
+ if response not in ("y", "yes"):
1234
+ print("Cancelled. Choose another directory or delete the existing one.")
1235
+ return 1
1236
+ # User agreed to overwrite - clear the entire directory including hidden files
1237
+ print(f"Clearing {destination}...")
1238
+ try:
1239
+ # Remove all contents including hidden files (.env, .git, etc.)
1240
+ shutil.rmtree(destination)
1241
+ except Exception as e:
1242
+ print(f"Error clearing directory: {e}")
1243
+ print("Please manually remove the directory and try again.")
1244
+ return 1
1245
+ # Recreate empty directory
1246
+ destination.mkdir(parents=True, exist_ok=True)
1247
+ # Verify it's actually empty
1248
+ if any(destination.iterdir()):
1249
+ print(f"Warning: Directory {destination} still contains files after clearing.")
1250
+ print("Some files may not have been removed. Please check manually.")
1251
+ return 1
1252
+ directory_cleared = True
1253
+ else:
1254
+ destination.mkdir(parents=True, exist_ok=True)
1255
+
1256
+ if selected.requires_modal:
1257
+ _ensure_modal_installed()
1258
+
1259
+ try:
1260
+ for spec in selected.iter_copy_specs():
1261
+ src_path = spec.absolute_source()
1262
+ if not src_path.exists():
1263
+ print(f"Template source missing: {src_path}")
1264
+ return 1
1265
+ dest_path = (destination / spec.destination).resolve()
1266
+
1267
+ # Handle directory copying
1268
+ if src_path.is_dir():
1269
+ if dest_path.exists() and not directory_cleared:
1270
+ if force:
1271
+ response = "y"
1272
+ else:
1273
+ try:
1274
+ response = (
1275
+ input(f"Directory {dest_path.name} exists. Overwrite? [y/N]: ")
1276
+ .strip()
1277
+ .lower()
1278
+ )
1279
+ except (EOFError, KeyboardInterrupt):
1280
+ print("\nCancelled.")
1281
+ return 1
1282
+ if response not in ("y", "yes"):
1283
+ print(f"Skipping {dest_path.name}")
1284
+ continue
1285
+ shutil.rmtree(dest_path)
1286
+ elif dest_path.exists() and directory_cleared:
1287
+ shutil.rmtree(dest_path)
1288
+ shutil.copytree(src_path, dest_path)
1289
+ else:
1290
+ # Handle file copying
1291
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
1292
+ if dest_path.exists() and not directory_cleared:
1293
+ if force:
1294
+ response = "y"
1295
+ else:
1296
+ try:
1297
+ response = (
1298
+ input(f"File {dest_path.name} exists. Overwrite? [y/N]: ")
1299
+ .strip()
1300
+ .lower()
1301
+ )
1302
+ except (EOFError, KeyboardInterrupt):
1303
+ print("\nCancelled.")
1304
+ return 1
1305
+ if response not in ("y", "yes"):
1306
+ print(f"Skipping {dest_path.name}")
1307
+ continue
1308
+ shutil.copy2(src_path, dest_path)
1309
+ if spec.make_executable:
1310
+ try:
1311
+ st = os.stat(dest_path)
1312
+ os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1313
+ except Exception:
1314
+ pass
1065
1315
 
1066
- print("Initialized Math Task App in synth_demo/:")
1067
- print(f" - {dst_task_py}")
1068
- print(f" - {dst_deploy}")
1069
- print(f" - {env_path} (created if missing)")
1070
- if os.path.exists(dst_cfg):
1071
- print(f" - {dst_cfg} (seeded)")
1072
- print("")
1073
- print("Next steps:")
1074
- print(" 1) cd synth_demo && put your ENVIRONMENT_API_KEY in ./.env")
1075
- print(" 2) Deploy to Modal:")
1076
- print(" uvx bash ./deploy_task_app.sh")
1077
- print(" 3) From project root, run: uvx synth-ai run")
1316
+ if selected.env_lines:
1317
+ env_path = destination / ".env"
1318
+ should_write = True
1319
+ if env_path.exists() and not directory_cleared:
1320
+ if force:
1321
+ response = "y"
1322
+ else:
1323
+ try:
1324
+ response = input("File .env exists. Overwrite? [y/N]: ").strip().lower()
1325
+ except (EOFError, KeyboardInterrupt):
1326
+ print("\nCancelled.")
1327
+ return 1
1328
+ should_write = response in ("y", "yes")
1329
+ if should_write:
1330
+ _write_text(str(env_path), "\n".join(selected.env_lines) + "\n")
1331
+ elif not directory_cleared:
1332
+ print("Skipping .env")
1333
+
1334
+ config_src = selected.config_source_path()
1335
+ if config_src and config_src.exists():
1336
+ cfg_dst = (destination / selected.config_destination).resolve()
1337
+ should_copy = True
1338
+ if cfg_dst.exists() and not directory_cleared:
1339
+ if force:
1340
+ response = "y"
1341
+ else:
1342
+ try:
1343
+ response = (
1344
+ input(f"File {cfg_dst.name} exists. Overwrite? [y/N]: ").strip().lower()
1345
+ )
1346
+ except (EOFError, KeyboardInterrupt):
1347
+ print("\nCancelled.")
1348
+ return 1
1349
+ should_copy = response in ("y", "yes")
1350
+ if should_copy:
1351
+ cfg_dst.parent.mkdir(parents=True, exist_ok=True)
1352
+ shutil.copy2(config_src, cfg_dst)
1353
+ elif not directory_cleared:
1354
+ print(f"Skipping {cfg_dst.name}")
1355
+
1356
+ if selected.post_copy is not None:
1357
+ try:
1358
+ selected.post_copy(destination)
1359
+ except Exception as post_exc:
1360
+ print(f"Post-processing failed: {post_exc}")
1361
+ return 1
1362
+
1363
+ # Store demo directory for subsequent commands
1364
+ demo_core.persist_demo_dir(str(destination))
1365
+
1366
+ # Store .env path if it was created
1367
+ env_file = destination / ".env"
1368
+ if env_file.exists():
1369
+ demo_core.persist_env_file_path(str(env_file))
1370
+
1371
+ print(f"Demo template '{selected.name}' materialised at {destination}.")
1372
+ print("Files created:")
1373
+ for spec in selected.iter_copy_specs():
1374
+ print(f" - {spec.destination}")
1375
+ if selected.env_lines:
1376
+ print(" - .env")
1377
+ if selected.config_source_path():
1378
+ print(f" - {selected.config_destination}")
1379
+ print("\nDemo directory stored. Subsequent commands will use this directory automatically.")
1380
+ print("Review the files, edit .env, and run any provided deploy scripts when ready.")
1078
1381
  return 0
1079
- except Exception as e:
1080
- print(f"Init error: {e}")
1081
- return 2
1382
+ except KeyboardInterrupt:
1383
+ print("Aborted")
1384
+ return 1
1385
+ except Exception as exc:
1386
+ print(f"Init failed: {exc}")
1387
+ return 1
1388
+
1082
1389
 
1390
+ def _http(
1391
+ method: str, url: str, headers: dict[str, str] | None = None, body: dict[str, Any] | None = None
1392
+ ) -> tuple[int, dict[str, Any] | str]:
1393
+ import json as _json
1394
+ import ssl
1395
+ import urllib.error
1396
+ import urllib.request
1083
1397
 
1084
- def _http(method: str, url: str, headers: Dict[str, str] | None = None, body: Dict[str, Any] | None = None) -> tuple[int, Dict[str, Any] | str]:
1085
- import urllib.request, urllib.error, json as _json, ssl
1086
1398
  data = None
1087
1399
  if body is not None:
1088
1400
  data = _json.dumps(body).encode("utf-8")
@@ -1119,10 +1431,23 @@ def _write_text(path: str, content: str) -> None:
1119
1431
  # Note: `prepare` command has been removed; configuration now prepares TOML
1120
1432
 
1121
1433
 
1122
- def cmd_run(args: argparse.Namespace) -> int:
1434
+ def run(
1435
+ config: str | None = None,
1436
+ batch_size: int | None = None,
1437
+ group_size: int | None = None,
1438
+ model: str | None = None,
1439
+ timeout: int = 600,
1440
+ dry_run: bool = False,
1441
+ ) -> int:
1442
+ # Change to demo directory if stored
1443
+ demo_dir = demo_core.load_demo_dir()
1444
+ if demo_dir and os.path.isdir(demo_dir):
1445
+ os.chdir(demo_dir)
1446
+ print(f"Using demo directory: {demo_dir}")
1447
+
1123
1448
  env = demo_core.load_env()
1124
1449
  cwd_env_path = os.path.join(os.getcwd(), ".env")
1125
- local_env = demo_core.load_dotenv_file(cwd_env_path)
1450
+ demo_core.load_dotenv_file(cwd_env_path)
1126
1451
 
1127
1452
  synth_key = (env.synth_api_key or "").strip()
1128
1453
  if not synth_key:
@@ -1154,7 +1479,7 @@ def cmd_run(args: argparse.Namespace) -> int:
1154
1479
  import tomllib
1155
1480
 
1156
1481
  try:
1157
- cfg_path = _select_or_create_config(getattr(args, "config", None), env)
1482
+ cfg_path = _select_or_create_config(config, env)
1158
1483
  except FileNotFoundError as exc:
1159
1484
  print(exc)
1160
1485
  return 1
@@ -1162,7 +1487,11 @@ def cmd_run(args: argparse.Namespace) -> int:
1162
1487
  # Detect monorepo launcher and delegate if available (aligns with run_clustered.sh which works)
1163
1488
  launcher = "/Users/joshpurtell/Documents/GitHub/monorepo/tests/applications/math/rl/start_math_clustered.py"
1164
1489
  if os.path.isfile(launcher):
1165
- backend_base = env.dev_backend_url[:-4] if env.dev_backend_url.endswith("/api") else env.dev_backend_url
1490
+ backend_base = (
1491
+ env.dev_backend_url[:-4]
1492
+ if env.dev_backend_url.endswith("/api")
1493
+ else env.dev_backend_url
1494
+ )
1166
1495
  run_env = os.environ.copy()
1167
1496
  run_env["BACKEND_URL"] = backend_base
1168
1497
  run_env["SYNTH_API_KEY"] = env.synth_api_key
@@ -1170,14 +1499,14 @@ def cmd_run(args: argparse.Namespace) -> int:
1170
1499
  run_env["ENVIRONMENT_API_KEY"] = env.env_api_key
1171
1500
  run_env["RL_CONFIG_PATH"] = cfg_path
1172
1501
  # Optional: TRAINER_START_URL passthrough if already set in environment
1173
- run_env["TRAINER_START_URL"] = run_env.get("TRAINER_START_URL", "")
1502
+ run_env["TRAINER_START_URL"] = run_env.get("TRAINER_START_URL", "") # type: ignore[misc]
1174
1503
  # Forward convenience knobs
1175
- if args.batch_size is not None:
1176
- run_env["RL_BATCH_SIZE"] = str(int(args.batch_size))
1177
- if args.group_size is not None:
1178
- run_env["RL_GROUP_SIZE"] = str(int(args.group_size))
1179
- if args.model:
1180
- run_env["RL_MODEL"] = args.model
1504
+ if batch_size is not None:
1505
+ run_env["RL_BATCH_SIZE"] = str(int(batch_size))
1506
+ if group_size is not None:
1507
+ run_env["RL_GROUP_SIZE"] = str(int(group_size))
1508
+ if model:
1509
+ run_env["RL_MODEL"] = model
1181
1510
  cmd = ["uv", "run", "python", launcher]
1182
1511
  print(f"Launching monorepo clustered runner: {' '.join(cmd)}")
1183
1512
  code = _popen_stream(cmd, env=run_env)
@@ -1192,33 +1521,33 @@ def cmd_run(args: argparse.Namespace) -> int:
1192
1521
  ek = (env.env_api_key or "").strip()
1193
1522
  print("Hint: If backend responded 401, verify SYNTH_API_KEY for:", base_url)
1194
1523
  if sk:
1195
- print(f" SYNTH_API_KEY len={len(sk)} last5={sk[-5:]}")
1524
+ print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1196
1525
  if ek:
1197
- print(f" ENVIRONMENT_API_KEY len={len(ek)} last5={ek[-5:]}")
1198
- print("Also ensure your Modal secret contains ENVIRONMENT_API_KEY and matches the task app.")
1526
+ print(f" {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
1527
+ print(
1528
+ "Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported."
1529
+ )
1199
1530
  return code
1200
1531
 
1201
1532
  # Fallback: legacy jobs API flow
1202
1533
  with open(cfg_path, "rb") as fh:
1203
1534
  inline_cfg = tomllib.load(fh)
1204
- with open(cfg_path, "r") as fh2:
1535
+ with open(cfg_path) as fh2:
1205
1536
  toml_text = fh2.read()
1206
- if args.batch_size is not None:
1207
- inline_cfg.setdefault("training", {})["batch_size"] = int(args.batch_size)
1208
- if args.group_size is not None:
1209
- inline_cfg.setdefault("training", {})["group_size"] = int(args.group_size)
1210
- model_name = args.model or (inline_cfg.get("model", {}) or {}).get("name", "Qwen/Qwen3-0.6B")
1537
+ if batch_size is not None:
1538
+ inline_cfg.setdefault("training", {})["batch_size"] = int(batch_size)
1539
+ if group_size is not None:
1540
+ inline_cfg.setdefault("training", {})["group_size"] = int(group_size)
1541
+ model_name = model or (inline_cfg.get("model", {}) or {}).get("name", "Qwen/Qwen3-0.6B") # type: ignore[misc]
1211
1542
  api = env.dev_backend_url.rstrip("/") + ("" if env.dev_backend_url.endswith("/api") else "/api")
1212
1543
  # Print backend and key preview before request for clearer diagnostics
1213
1544
  try:
1214
1545
  sk = (env.synth_api_key or "").strip()
1215
- sk_len = len(sk)
1216
- sk_tail = sk[-5:] if sk_len >= 5 else sk
1217
1546
  print(f"[run] Backend API: {api}")
1218
- print(f"[run] Using SYNTH_API_KEY len={sk_len} last5={sk_tail}")
1547
+ print(f"[run] {_key_preview(sk, 'SYNTH_API_KEY')}")
1219
1548
  except Exception:
1220
1549
  pass
1221
- data_fragment: Dict[str, Any] = {
1550
+ data_fragment: dict[str, Any] = {
1222
1551
  "model": model_name,
1223
1552
  "endpoint_base_url": env.task_app_base_url,
1224
1553
  "config": inline_cfg,
@@ -1229,30 +1558,35 @@ def cmd_run(args: argparse.Namespace) -> int:
1229
1558
  if env.env_api_key:
1230
1559
  data_fragment["environment_api_key"] = env.env_api_key
1231
1560
  for k in ("training", "evaluation", "rollout", "topology", "vllm"):
1232
- if isinstance(inline_cfg.get(k), dict):
1561
+ if isinstance(inline_cfg.get(k), dict): # type: ignore[misc]
1233
1562
  data_fragment[k] = inline_cfg[k]
1234
1563
  compute = {}
1235
- if isinstance(inline_cfg.get("compute"), dict):
1236
- if inline_cfg["compute"].get("gpu_type"):
1564
+ if isinstance(inline_cfg.get("compute"), dict): # type: ignore[misc]
1565
+ if inline_cfg["compute"].get("gpu_type"): # type: ignore[misc]
1237
1566
  compute["gpu_type"] = str(inline_cfg["compute"]["gpu_type"]).upper()
1238
- if inline_cfg["compute"].get("gpu_count"):
1239
- compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1567
+ if inline_cfg["compute"].get("gpu_count"): # type: ignore[misc]
1568
+ compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1240
1569
  if not compute:
1241
- topo = inline_cfg.get("topology") or {}
1242
- gshape = str(topo.get("gpu_type") or "")
1570
+ topo = inline_cfg.get("topology") or {} # type: ignore[misc]
1571
+ gshape = str(topo.get("gpu_type") or "") # type: ignore[misc]
1243
1572
  if ":" in gshape:
1244
1573
  t, c = gshape.split(":", 1)
1245
1574
  compute = {"gpu_type": t.upper(), "gpu_count": int(c)}
1246
- body: Dict[str, Any] = {
1575
+ body: dict[str, Any] = {
1247
1576
  "job_type": "rl",
1248
1577
  "data": data_fragment,
1249
1578
  }
1250
1579
  if compute:
1251
1580
  body["compute"] = compute
1252
- code, js = _http("POST", api + "/rl/jobs", headers={
1253
- "Content-Type": "application/json",
1254
- "Authorization": f"Bearer {env.synth_api_key}",
1255
- }, body=body)
1581
+ code, js = _http(
1582
+ "POST",
1583
+ api + "/rl/jobs",
1584
+ headers={
1585
+ "Content-Type": "application/json",
1586
+ "Authorization": f"Bearer {env.synth_api_key}",
1587
+ },
1588
+ body=body,
1589
+ )
1256
1590
  if code not in (200, 201) or not isinstance(js, dict):
1257
1591
  print("Job create failed:", code)
1258
1592
  print(f"Backend: {api}")
@@ -1264,19 +1598,81 @@ def cmd_run(args: argparse.Namespace) -> int:
1264
1598
  except Exception:
1265
1599
  print(str(js))
1266
1600
  print("Request body was:\n" + json.dumps(body, indent=2))
1601
+ try:
1602
+ auth_preview = _key_preview(env.synth_api_key or "", "SYNTH_API_KEY (auth)")
1603
+ print(f"[run] {auth_preview}")
1604
+ except Exception:
1605
+ pass
1606
+ try:
1607
+ data_block = body.get("data") if isinstance(body, dict) else None # type: ignore[misc]
1608
+ env_key_body = ""
1609
+ if isinstance(data_block, dict):
1610
+ env_key_body = str(data_block.get("environment_api_key") or "") # type: ignore[misc]
1611
+ if env_key_body:
1612
+ print(f"[run] {_key_preview(env_key_body, 'environment_api_key (body)')}")
1613
+ except Exception:
1614
+ pass
1615
+ try:
1616
+ current_env_key = env.env_api_key or ""
1617
+ if current_env_key:
1618
+ print(f"[run] {_key_preview(current_env_key, 'ENVIRONMENT_API_KEY (current)')}")
1619
+ except Exception:
1620
+ pass
1621
+ if isinstance(js, dict):
1622
+ detail = js.get("detail") # type: ignore[misc]
1623
+ if isinstance(detail, dict):
1624
+ try:
1625
+ sent_key = detail.get("sent_key") # type: ignore[misc]
1626
+ if isinstance(sent_key, str):
1627
+ print(
1628
+ f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}"
1629
+ )
1630
+ except Exception:
1631
+ pass
1632
+ try:
1633
+ sent_keys = detail.get("sent_keys") # type: ignore[misc]
1634
+ if isinstance(sent_keys, list | tuple):
1635
+ previews = []
1636
+ for idx, val in enumerate(sent_keys):
1637
+ if isinstance(val, str):
1638
+ previews.append(_key_preview(val, f"detail.sent_keys[{idx}]"))
1639
+ if previews:
1640
+ joined = "; ".join(previews)
1641
+ print(f"[run] Backend detail.sent_keys previews: {joined}")
1642
+ except Exception:
1643
+ pass
1644
+ try:
1645
+ key_prefix = detail.get("sent_key_prefix") # type: ignore[misc]
1646
+ if isinstance(key_prefix, str):
1647
+ print(f"[run] Backend detail.sent_key_prefix={key_prefix}")
1648
+ except Exception:
1649
+ pass
1650
+ try:
1651
+ health_url = detail.get("health_url")
1652
+ if isinstance(health_url, str):
1653
+ print(f"[run] Backend detail.health_url={health_url}")
1654
+ except Exception:
1655
+ pass
1267
1656
  # Extra hints for auth failures
1268
1657
  try:
1269
1658
  sk = (env.synth_api_key or "").strip()
1270
- if int(code) == 401 or (isinstance(js, dict) and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())):
1659
+ if int(code) == 401 or (
1660
+ isinstance(js, dict)
1661
+ and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())
1662
+ ):
1271
1663
  base_url = env.dev_backend_url
1272
- print("Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url)
1664
+ print(
1665
+ "Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url
1666
+ )
1273
1667
  if sk:
1274
- print(f" SYNTH_API_KEY len={len(sk)} last5={sk[-5:]}")
1275
- print("Also ensure your Modal secret contains a valid ENVIRONMENT_API_KEY.")
1668
+ print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1669
+ print(
1670
+ "Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid."
1671
+ )
1276
1672
  except Exception:
1277
1673
  pass
1278
1674
  return 2
1279
- job_id = js.get("job_id") or js.get("id") or ""
1675
+ job_id = js.get("job_id") or js.get("id") or "" # type: ignore[misc]
1280
1676
  if not job_id:
1281
1677
  print("Job id missing in response:", js)
1282
1678
  print("Request body was:\n" + json.dumps(body, indent=2))
@@ -1296,7 +1692,7 @@ def cmd_run(args: argparse.Namespace) -> int:
1296
1692
  start_t = time.time()
1297
1693
  while True:
1298
1694
  sc, sj = _http("GET", api + f"/learning/jobs/{job_id}")
1299
- status = (sj.get("status") if isinstance(sj, dict) else "") if sc == 200 else ""
1695
+ status = (sj.get("status") if isinstance(sj, dict) else "") if sc == 200 else "" # type: ignore[misc]
1300
1696
  if status and status != last_status:
1301
1697
  last_status = status
1302
1698
  print("status →", status)
@@ -1308,14 +1704,14 @@ def cmd_run(args: argparse.Namespace) -> int:
1308
1704
  api + f"/orchestration/jobs/{job_id}/events?since_seq={since}&limit=200",
1309
1705
  )
1310
1706
  if ec == 200 and isinstance(ej, dict):
1311
- events = ej.get("events") or ej.get("data") or []
1707
+ events = ej.get("events") or ej.get("data") or [] # type: ignore[misc]
1312
1708
  for e in events:
1313
1709
  seq = int(e.get("seq") or 0)
1314
1710
  if seq <= since:
1315
1711
  continue
1316
1712
  since = seq
1317
- typ = str(e.get("type") or e.get("event_type") or "").lower()
1318
- msg = e.get("message") or e.get("msg") or ""
1713
+ typ = str(e.get("type") or e.get("event_type") or "").lower() # type: ignore[misc]
1714
+ msg = e.get("message") or e.get("msg") or "" # type: ignore[misc]
1319
1715
  if typ in (
1320
1716
  "rl.eval.started",
1321
1717
  "rl.eval.summary",
@@ -1324,70 +1720,16 @@ def cmd_run(args: argparse.Namespace) -> int:
1324
1720
  "rl.performance.metrics",
1325
1721
  ):
1326
1722
  print(f"[{seq}] {typ}: {msg}")
1327
- mc, mj = _http(
1328
- "GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50"
1329
- )
1723
+ mc, mj = _http("GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50")
1330
1724
  if mc == 200 and isinstance(mj, dict):
1331
- pts = mj.get("points") or []
1725
+ pts = mj.get("points") or [] # type: ignore[misc]
1332
1726
  for p in pts:
1333
- name = p.get("name")
1727
+ name = p.get("name") # type: ignore[misc]
1334
1728
  if name == "eval.reward_mean":
1335
- print(f"metric eval.reward_mean step={p.get('step')} value={p.get('value')}")
1729
+ print(f"metric eval.reward_mean step={p.get('step')} value={p.get('value')}") # type: ignore[misc]
1336
1730
  break
1337
- if time.time() - start_t > (args.timeout or 600):
1731
+ if time.time() - start_t > (timeout or 600):
1338
1732
  print("Timeout waiting for terminal state.")
1339
1733
  break
1340
1734
  time.sleep(2)
1341
1735
  return 0
1342
-
1343
-
1344
- def main(argv: list[str] | None = None) -> int:
1345
- p = argparse.ArgumentParser(prog="synth-ai")
1346
- sub = p.add_subparsers(dest="cmd")
1347
-
1348
- def _add_parser(names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]) -> None:
1349
- for name in names:
1350
- parser = sub.add_parser(name)
1351
- configure(parser)
1352
-
1353
- _add_parser(["rl_demo.setup", "demo.setup"], configure=lambda parser: parser.set_defaults(func=cmd_setup))
1354
-
1355
- def _init_opts(parser):
1356
- parser.add_argument("--force", action="store_true", help="Overwrite existing files in CWD")
1357
- parser.set_defaults(func=cmd_init)
1358
-
1359
- _add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
1360
-
1361
- # (prepare command removed)
1362
-
1363
- def _deploy_opts(parser):
1364
- parser.add_argument("--local", action="store_true", help="Run local FastAPI instead of Modal deploy")
1365
- parser.add_argument("--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy")
1366
- parser.add_argument("--name", type=str, default="synth-math-demo", help="Modal app name")
1367
- parser.add_argument("--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)")
1368
- parser.set_defaults(func=cmd_deploy)
1369
-
1370
- _add_parser(["rl_demo.deploy", "demo.deploy"], configure=_deploy_opts)
1371
-
1372
- _add_parser(["rl_demo.configure", "demo.configure"], configure=lambda parser: parser.set_defaults(func=cmd_run))
1373
-
1374
- def _run_opts(parser):
1375
- parser.add_argument("--config", type=str, default=None, help="Path to TOML config (skip prompt)")
1376
- parser.add_argument("--batch-size", type=int, default=None)
1377
- parser.add_argument("--group-size", type=int, default=None)
1378
- parser.add_argument("--model", type=str, default=None)
1379
- parser.add_argument("--timeout", type=int, default=600)
1380
- parser.add_argument("--dry-run", action="store_true", help="Print request body and exit")
1381
- parser.set_defaults(func=cmd_run)
1382
-
1383
- _add_parser(["run", "rl_demo.run", "demo.run"], configure=_run_opts)
1384
-
1385
- args = p.parse_args(argv)
1386
- if not hasattr(args, "func"):
1387
- p.print_help()
1388
- return 1
1389
- return int(args.func(args) or 0)
1390
-
1391
-
1392
- if __name__ == "__main__":
1393
- sys.exit(main())