synth-ai 0.2.14__py3-none-any.whl → 0.2.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (354) hide show
  1. examples/README.md +1 -0
  2. examples/analyze_semantic_words.sh +2 -2
  3. examples/blog_posts/pokemon_vl/README.md +98 -0
  4. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
  5. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  6. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  7. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
  8. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  9. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  10. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  11. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  12. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  13. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  14. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
  15. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  16. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  17. examples/multi_step/SFT_README.md +147 -0
  18. examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
  19. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +73 -115
  20. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
  21. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
  22. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  23. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  24. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  25. examples/multi_step/convert_traces_to_sft.py +84 -0
  26. examples/multi_step/run_sft_qwen30b.sh +45 -0
  27. examples/qwen_coder/configs/coder_lora_30b.toml +1 -2
  28. examples/qwen_coder/configs/coder_lora_4b.toml +5 -1
  29. examples/qwen_coder/configs/coder_lora_small.toml +1 -2
  30. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  31. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  32. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  33. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  34. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  35. examples/qwen_vl/QUICKSTART.md +327 -0
  36. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  37. examples/qwen_vl/README.md +152 -0
  38. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  39. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  40. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  41. examples/qwen_vl/SETUP_COMPLETE.md +274 -0
  42. examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
  43. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  44. examples/qwen_vl/__init__.py +2 -0
  45. examples/qwen_vl/collect_data_via_cli.md +415 -0
  46. examples/qwen_vl/collect_vision_traces.py +368 -0
  47. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
  48. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
  49. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
  50. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  51. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
  52. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  53. examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
  54. examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
  55. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  56. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  57. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  58. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  59. examples/qwen_vl/run_vision_comparison.sh +61 -0
  60. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  61. examples/qwen_vl/test_image_validation.py +201 -0
  62. examples/qwen_vl/test_sft_vision_data.py +110 -0
  63. examples/rl/README.md +6 -6
  64. examples/rl/configs/eval_base_qwen.toml +17 -0
  65. examples/rl/configs/eval_rl_qwen.toml +13 -0
  66. examples/rl/configs/rl_from_base_qwen.toml +62 -0
  67. examples/rl/configs/rl_from_base_qwen17.toml +79 -0
  68. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  69. examples/rl/run_eval.py +436 -0
  70. examples/rl/run_rl_and_save.py +111 -0
  71. examples/rl/task_app/README.md +21 -0
  72. examples/rl/task_app/math_single_step.py +990 -0
  73. examples/rl/task_app/math_task_app.py +111 -0
  74. examples/run_crafter_demo.sh +2 -2
  75. examples/sft/README.md +6 -6
  76. examples/sft/configs/crafter_fft_qwen0p6b.toml +7 -2
  77. examples/sft/configs/crafter_lora_qwen0p6b.toml +7 -3
  78. examples/sft/evaluate.py +2 -4
  79. examples/sft/export_dataset.py +7 -4
  80. examples/swe/task_app/README.md +33 -3
  81. examples/swe/task_app/grpo_swe_mini.py +4 -1
  82. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  83. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  84. examples/swe/task_app/hosted/envs/mini_swe/environment.py +50 -23
  85. examples/swe/task_app/hosted/inference/openai_client.py +4 -4
  86. examples/swe/task_app/hosted/policy_routes.py +0 -2
  87. examples/swe/task_app/hosted/rollout.py +0 -8
  88. examples/swe/task_app/morph_backend.py +178 -0
  89. examples/task_apps/crafter/task_app/README.md +1 -1
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +70 -10
  91. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +63 -27
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +48 -50
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +75 -36
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +31 -15
  97. examples/task_apps/enron/__init__.py +1 -0
  98. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  99. examples/task_apps/math/README.md +1 -2
  100. examples/task_apps/pokemon_red/README.md +3 -4
  101. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  102. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  103. examples/task_apps/pokemon_red/task_app.py +36 -5
  104. examples/task_apps/sokoban/README.md +2 -3
  105. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  106. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  107. examples/vlm/README.md +3 -3
  108. examples/vlm/configs/crafter_vlm_gpt4o.toml +5 -0
  109. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  110. examples/vlm/filter_image_rows.py +1 -1
  111. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  112. examples/warming_up_to_rl/_utils.py +92 -0
  113. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  114. examples/warming_up_to_rl/configs/crafter_fft.toml +5 -0
  115. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  116. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  117. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  118. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  119. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  120. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  121. examples/warming_up_to_rl/readme.md +63 -132
  122. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  123. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  124. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  125. examples/warming_up_to_rl/task_app/README.md +42 -0
  126. examples/warming_up_to_rl/task_app/grpo_crafter.py +827 -0
  127. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  128. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  129. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  130. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  131. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  132. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  133. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  134. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  135. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  136. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
  137. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  138. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  139. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1084 -0
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  147. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  148. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  152. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  153. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
  154. synth_ai/__init__.py +44 -30
  155. synth_ai/_utils/__init__.py +47 -0
  156. synth_ai/_utils/base_url.py +10 -0
  157. synth_ai/_utils/http.py +10 -0
  158. synth_ai/_utils/prompts.py +10 -0
  159. synth_ai/_utils/task_app_state.py +12 -0
  160. synth_ai/_utils/user_config.py +10 -0
  161. synth_ai/api/models/supported.py +144 -7
  162. synth_ai/api/train/__init__.py +13 -1
  163. synth_ai/api/train/builders.py +9 -3
  164. synth_ai/api/train/cli.py +155 -17
  165. synth_ai/api/train/config_finder.py +18 -11
  166. synth_ai/api/train/configs/__init__.py +8 -1
  167. synth_ai/api/train/configs/rl.py +32 -7
  168. synth_ai/api/train/configs/sft.py +6 -2
  169. synth_ai/api/train/configs/shared.py +59 -2
  170. synth_ai/api/train/env_resolver.py +13 -10
  171. synth_ai/auth/credentials.py +119 -0
  172. synth_ai/cli/__init__.py +61 -69
  173. synth_ai/cli/_modal_wrapper.py +7 -5
  174. synth_ai/cli/_typer_patch.py +0 -2
  175. synth_ai/cli/_validate_task_app.py +22 -4
  176. synth_ai/cli/commands/__init__.py +17 -0
  177. synth_ai/cli/commands/demo/__init__.py +6 -0
  178. synth_ai/cli/commands/demo/core.py +163 -0
  179. synth_ai/cli/commands/deploy/__init__.py +23 -0
  180. synth_ai/cli/commands/deploy/core.py +614 -0
  181. synth_ai/cli/commands/deploy/errors.py +72 -0
  182. synth_ai/cli/commands/deploy/validation.py +11 -0
  183. synth_ai/cli/commands/eval/__init__.py +19 -0
  184. synth_ai/cli/commands/eval/core.py +1109 -0
  185. synth_ai/cli/commands/eval/errors.py +81 -0
  186. synth_ai/cli/commands/eval/validation.py +133 -0
  187. synth_ai/cli/commands/filter/__init__.py +12 -0
  188. synth_ai/cli/commands/filter/core.py +388 -0
  189. synth_ai/cli/commands/filter/errors.py +55 -0
  190. synth_ai/cli/commands/filter/validation.py +77 -0
  191. synth_ai/cli/commands/help/__init__.py +177 -0
  192. synth_ai/cli/commands/help/core.py +73 -0
  193. synth_ai/cli/commands/status/__init__.py +64 -0
  194. synth_ai/cli/commands/status/client.py +192 -0
  195. synth_ai/cli/commands/status/config.py +92 -0
  196. synth_ai/cli/commands/status/errors.py +20 -0
  197. synth_ai/cli/commands/status/formatters.py +164 -0
  198. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  199. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  200. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  201. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  202. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  203. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  204. synth_ai/cli/commands/status/utils.py +114 -0
  205. synth_ai/cli/commands/train/__init__.py +53 -0
  206. synth_ai/cli/commands/train/core.py +21 -0
  207. synth_ai/cli/commands/train/errors.py +117 -0
  208. synth_ai/cli/commands/train/judge_schemas.py +199 -0
  209. synth_ai/cli/commands/train/judge_validation.py +304 -0
  210. synth_ai/cli/commands/train/validation.py +443 -0
  211. synth_ai/cli/demo.py +2 -162
  212. synth_ai/cli/deploy/__init__.py +28 -0
  213. synth_ai/cli/deploy/core.py +5 -0
  214. synth_ai/cli/deploy/errors.py +23 -0
  215. synth_ai/cli/deploy/validation.py +5 -0
  216. synth_ai/cli/eval/__init__.py +36 -0
  217. synth_ai/cli/eval/core.py +5 -0
  218. synth_ai/cli/eval/errors.py +31 -0
  219. synth_ai/cli/eval/validation.py +5 -0
  220. synth_ai/cli/filter/__init__.py +28 -0
  221. synth_ai/cli/filter/core.py +5 -0
  222. synth_ai/cli/filter/errors.py +23 -0
  223. synth_ai/cli/filter/validation.py +5 -0
  224. synth_ai/cli/legacy_root_backup.py +3 -1
  225. synth_ai/cli/lib/__init__.py +10 -0
  226. synth_ai/cli/lib/task_app_discovery.py +7 -0
  227. synth_ai/cli/lib/task_app_env.py +518 -0
  228. synth_ai/cli/modal_serve/__init__.py +12 -0
  229. synth_ai/cli/modal_serve/core.py +14 -0
  230. synth_ai/cli/modal_serve/errors.py +8 -0
  231. synth_ai/cli/modal_serve/validation.py +11 -0
  232. synth_ai/cli/recent.py +2 -1
  233. synth_ai/cli/serve/__init__.py +12 -0
  234. synth_ai/cli/serve/core.py +14 -0
  235. synth_ai/cli/serve/errors.py +8 -0
  236. synth_ai/cli/serve/validation.py +11 -0
  237. synth_ai/cli/setup.py +21 -0
  238. synth_ai/cli/status.py +7 -126
  239. synth_ai/cli/task_app_deploy.py +7 -0
  240. synth_ai/cli/task_app_list.py +25 -0
  241. synth_ai/cli/task_app_modal_serve.py +11 -0
  242. synth_ai/cli/task_app_serve.py +11 -0
  243. synth_ai/cli/task_apps.py +110 -1499
  244. synth_ai/cli/traces.py +1 -1
  245. synth_ai/cli/train/__init__.py +12 -0
  246. synth_ai/cli/train/core.py +21 -0
  247. synth_ai/cli/train/errors.py +8 -0
  248. synth_ai/cli/train/validation.py +24 -0
  249. synth_ai/cli/train.py +5 -0
  250. synth_ai/cli/turso.py +1 -1
  251. synth_ai/cli/watch.py +1 -1
  252. synth_ai/demos/__init__.py +10 -0
  253. synth_ai/demos/core/__init__.py +28 -1
  254. synth_ai/demos/crafter/__init__.py +1 -0
  255. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  256. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  257. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  258. synth_ai/demos/demo_registry.py +176 -0
  259. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  260. synth_ai/demos/math/__init__.py +1 -0
  261. synth_ai/demos/math/_common.py +16 -0
  262. synth_ai/demos/math/app.py +38 -0
  263. synth_ai/demos/math/config.toml +76 -0
  264. synth_ai/demos/math/deploy_modal.py +54 -0
  265. synth_ai/demos/math/modal_task_app.py +702 -0
  266. synth_ai/demos/math/task_app_entry.py +51 -0
  267. synth_ai/environments/environment/core.py +7 -1
  268. synth_ai/environments/examples/bandit/engine.py +0 -1
  269. synth_ai/environments/examples/bandit/environment.py +0 -1
  270. synth_ai/environments/examples/red/engine.py +33 -12
  271. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  272. synth_ai/environments/examples/red/environment.py +26 -0
  273. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  274. synth_ai/environments/examples/wordle/environment.py +0 -1
  275. synth_ai/evals/base.py +16 -5
  276. synth_ai/evals/client.py +1 -1
  277. synth_ai/http.py +8 -22
  278. synth_ai/inference/client.py +1 -1
  279. synth_ai/judge_schemas.py +4 -5
  280. synth_ai/learning/client.py +1 -1
  281. synth_ai/learning/health.py +1 -1
  282. synth_ai/learning/jobs.py +1 -1
  283. synth_ai/learning/rl/client.py +4 -2
  284. synth_ai/learning/rl/env_keys.py +1 -1
  285. synth_ai/learning/rl/secrets.py +1 -1
  286. synth_ai/learning/sft/client.py +1 -1
  287. synth_ai/learning/sft/data.py +407 -4
  288. synth_ai/learning/validators.py +4 -1
  289. synth_ai/streaming/__init__.py +29 -0
  290. synth_ai/streaming/config.py +94 -0
  291. synth_ai/streaming/handlers.py +469 -0
  292. synth_ai/streaming/streamer.py +301 -0
  293. synth_ai/streaming/types.py +95 -0
  294. synth_ai/task/apps/__init__.py +4 -2
  295. synth_ai/task/config.py +6 -4
  296. synth_ai/task/rubrics/__init__.py +1 -2
  297. synth_ai/task/rubrics/loaders.py +14 -10
  298. synth_ai/task/rubrics.py +219 -0
  299. synth_ai/task/trace_correlation_helpers.py +24 -11
  300. synth_ai/task/tracing_utils.py +14 -3
  301. synth_ai/task/validators.py +0 -1
  302. synth_ai/tracing_v3/abstractions.py +3 -3
  303. synth_ai/tracing_v3/config.py +15 -13
  304. synth_ai/tracing_v3/constants.py +21 -0
  305. synth_ai/tracing_v3/db_config.py +3 -1
  306. synth_ai/tracing_v3/decorators.py +10 -7
  307. synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
  308. synth_ai/tracing_v3/migration_helper.py +1 -2
  309. synth_ai/tracing_v3/session_tracer.py +7 -7
  310. synth_ai/tracing_v3/storage/base.py +29 -29
  311. synth_ai/tracing_v3/storage/config.py +3 -3
  312. synth_ai/tracing_v3/turso/daemon.py +8 -9
  313. synth_ai/tracing_v3/turso/native_manager.py +80 -72
  314. synth_ai/tracing_v3/utils.py +2 -2
  315. synth_ai/utils/__init__.py +101 -0
  316. synth_ai/utils/base_url.py +94 -0
  317. synth_ai/utils/cli.py +131 -0
  318. synth_ai/utils/env.py +294 -0
  319. synth_ai/utils/http.py +172 -0
  320. synth_ai/utils/modal.py +308 -0
  321. synth_ai/utils/process.py +212 -0
  322. synth_ai/utils/prompts.py +39 -0
  323. synth_ai/utils/sqld.py +122 -0
  324. synth_ai/utils/task_app_discovery.py +882 -0
  325. synth_ai/utils/task_app_env.py +186 -0
  326. synth_ai/utils/task_app_state.py +318 -0
  327. synth_ai/utils/user_config.py +137 -0
  328. synth_ai/v0/config/__init__.py +1 -5
  329. synth_ai/v0/config/base_url.py +1 -7
  330. synth_ai/v0/tracing/config.py +1 -1
  331. synth_ai/v0/tracing/decorators.py +1 -1
  332. synth_ai/v0/tracing/upload.py +1 -1
  333. synth_ai/v0/tracing_v1/config.py +1 -1
  334. synth_ai/v0/tracing_v1/decorators.py +1 -1
  335. synth_ai/v0/tracing_v1/upload.py +1 -1
  336. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/METADATA +91 -32
  337. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/RECORD +341 -154
  338. synth_ai/cli/man.py +0 -106
  339. synth_ai/cli/tui.py +0 -57
  340. synth_ai/compound/cais.py +0 -0
  341. synth_ai/core/experiment.py +0 -13
  342. synth_ai/core/system.py +0 -15
  343. synth_ai/demo_registry.py +0 -295
  344. synth_ai/handshake.py +0 -109
  345. synth_ai/tui/__init__.py +0 -5
  346. synth_ai/tui/__main__.py +0 -13
  347. synth_ai/tui/cli/__init__.py +0 -1
  348. synth_ai/tui/cli/query_experiments.py +0 -164
  349. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  350. synth_ai/tui/dashboard.py +0 -906
  351. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
  352. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
  353. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
  354. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,518 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import os
5
+ import signal
6
+ import subprocess
7
+ import time
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+
11
+ import click
12
+ from click.exceptions import Abort
13
+ from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
14
+ from synth_ai.task.apps import TaskAppEntry
15
+
16
+ REPO_ROOT = Path(__file__).resolve().parents[3]
17
+
18
+
19
+ def load_env_files_into_process(paths: Sequence[str]) -> None:
20
+ """Load key/value pairs from .env-style files into the current process."""
21
+
22
+ for path_str in paths:
23
+ try:
24
+ content = Path(path_str).expanduser().read_text()
25
+ except Exception:
26
+ continue
27
+ for line in content.splitlines():
28
+ if not line or line.lstrip().startswith("#") or "=" not in line:
29
+ continue
30
+ key, value = line.split("=", 1)
31
+ key = key.strip()
32
+ val = value.strip().strip('"').strip("'")
33
+ if not key:
34
+ continue
35
+ current = os.environ.get(key, "")
36
+ if not current.strip():
37
+ os.environ[key] = val
38
+
39
+
40
+ def _collect_env_candidates(base_dir: Path) -> list[Path]:
41
+ cwd = Path.cwd()
42
+ candidates: list[Path] = []
43
+
44
+ candidates.extend(sorted(cwd.glob("**/*.env")))
45
+
46
+ repo_candidates = sorted(REPO_ROOT.glob("**/*.env"))
47
+ for candidate in repo_candidates:
48
+ if candidate not in candidates:
49
+ candidates.append(candidate)
50
+
51
+ if base_dir not in (cwd, REPO_ROOT):
52
+ base_candidates = sorted(base_dir.glob("**/*.env"))
53
+ for candidate in base_candidates:
54
+ if candidate not in candidates:
55
+ candidates.append(candidate)
56
+
57
+ return candidates
58
+
59
+
60
+ def determine_env_files(entry: TaskAppEntry, user_env_files: Sequence[str]) -> list[Path]:
61
+ """Resolve env file paths for a task app invocation."""
62
+
63
+ resolved: list[Path] = []
64
+ for candidate in user_env_files:
65
+ path = Path(candidate).expanduser()
66
+ if not path.exists():
67
+ raise click.ClickException(f"Env file not found: {path}")
68
+ resolved.append(path)
69
+ if resolved:
70
+ return resolved
71
+
72
+ candidates = _collect_env_candidates(Path.cwd())
73
+ if not candidates:
74
+ raise click.ClickException("No env file found. Pass --env-file explicitly.")
75
+
76
+ click.echo("Select env file to load:")
77
+ for idx, path in enumerate(candidates, start=1):
78
+ click.echo(f" {idx}) {path.resolve()}")
79
+ choice = click.prompt("Enter choice", type=click.IntRange(1, len(candidates)), default=1)
80
+ selected = candidates[choice - 1]
81
+ return [selected]
82
+
83
+
84
+ def resolve_env_paths_for_script(script_path: Path, explicit: Sequence[str]) -> list[Path]:
85
+ """Resolve env files for a standalone Modal script."""
86
+
87
+ if explicit:
88
+ resolved = []
89
+ for candidate in explicit:
90
+ path = Path(candidate).expanduser()
91
+ if not path.exists():
92
+ raise click.ClickException(f"Env file not found: {path}")
93
+ resolved.append(path)
94
+ return resolved
95
+
96
+ candidates = _collect_env_candidates(script_path.parent.resolve())
97
+ if not candidates:
98
+ created = interactive_create_env(script_path.parent)
99
+ if created is None:
100
+ raise click.ClickException("Env file required (--env-file) for this task app")
101
+ return [created]
102
+
103
+ click.echo("Select env file to load:")
104
+ for idx, path in enumerate(candidates, start=1):
105
+ click.echo(f" {idx}) {path.resolve()}")
106
+ choice = click.prompt("Enter choice", type=click.IntRange(1, len(candidates)), default=1)
107
+ return [candidates[choice - 1]]
108
+
109
+
110
+ def ensure_port_free(port: int, host: str, *, force: bool) -> None:
111
+ """Ensure a TCP port is not in use, optionally killing processes if --force."""
112
+
113
+ import socket # local import to avoid unnecessary dependency during CLI import
114
+
115
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
116
+ in_use = sock.connect_ex((host, port)) == 0
117
+ if not in_use:
118
+ return
119
+
120
+ try:
121
+ out = subprocess.run(
122
+ ["lsof", "-ti", f"TCP:{port}"],
123
+ capture_output=True,
124
+ text=True,
125
+ check=False,
126
+ )
127
+ pids = [pid for pid in out.stdout.strip().splitlines() if pid]
128
+ except FileNotFoundError:
129
+ pids = []
130
+
131
+ if not force:
132
+ message = f"Port {port} appears to be in use"
133
+ if pids:
134
+ message += f" (PIDs: {', '.join(pids)})"
135
+ raise click.ClickException(message)
136
+
137
+ for pid in pids:
138
+ try:
139
+ os.kill(int(pid), signal.SIGTERM)
140
+ except Exception as exc:
141
+ raise click.ClickException(f"Failed to terminate PID {pid}: {exc}") from exc
142
+
143
+ time.sleep(0.5)
144
+
145
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
146
+ still_in_use = sock.connect_ex((host, port)) == 0
147
+
148
+ if still_in_use:
149
+ for pid in pids:
150
+ try:
151
+ os.kill(int(pid), signal.SIGKILL)
152
+ except Exception as exc:
153
+ raise click.ClickException(f"Failed to force terminate PID {pid}: {exc}") from exc
154
+ time.sleep(0.5)
155
+
156
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
157
+ if sock.connect_ex((host, port)) == 0:
158
+ raise click.ClickException(
159
+ f"Port {port} is still in use after attempting to terminate processes."
160
+ )
161
+
162
+
163
+ def save_to_env_file(env_path: Path, key: str, value: str) -> None:
164
+ """Save or update a key/value pair in a .env file."""
165
+
166
+ try:
167
+ existing_lines = env_path.read_text().splitlines() if env_path.exists() else []
168
+ except Exception as exc:
169
+ raise click.ClickException(f"Failed to read {env_path}: {exc}") from exc
170
+
171
+ env_path.parent.mkdir(parents=True, exist_ok=True)
172
+
173
+ key_updated = False
174
+ updated_lines: list[str] = []
175
+ for line in existing_lines:
176
+ if line.strip().startswith(f"{key}="):
177
+ updated_lines.append(f"{key}={value}")
178
+ key_updated = True
179
+ else:
180
+ updated_lines.append(line)
181
+
182
+ if key_updated:
183
+ env_path.write_text("\n".join(updated_lines) + "\n")
184
+ click.echo(f"Updated {key} in {env_path}")
185
+ return
186
+
187
+ with env_path.open("a", encoding="utf-8") as handle:
188
+ if existing_lines and existing_lines[-1].strip():
189
+ handle.write("\n")
190
+ handle.write(f"{key}={value}\n")
191
+ click.echo(f"Saved {key} to {env_path}")
192
+
193
+
194
+ def persist_env_api_key(env_api_key: str, env_paths: Sequence[Path] | None) -> None:
195
+ """Persist ENVIRONMENT_API_KEY to provided .env files (or demo directory .env)."""
196
+
197
+ targets: list[Path] = []
198
+ seen: set[Path] = set()
199
+ for path in env_paths or ():
200
+ try:
201
+ resolved = Path(path).resolve()
202
+ except Exception:
203
+ continue
204
+ if resolved in seen:
205
+ continue
206
+ seen.add(resolved)
207
+ targets.append(resolved)
208
+
209
+ if not targets:
210
+ demo_dir = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
211
+ targets.append((demo_dir / ".env").resolve())
212
+
213
+ for target in targets:
214
+ save_to_env_file(target, "ENVIRONMENT_API_KEY", env_api_key)
215
+
216
+
217
+ def _load_dotenv_if_present(env_file: Path) -> None:
218
+ try:
219
+ from dotenv import load_dotenv
220
+ except Exception:
221
+ return
222
+
223
+ with contextlib.suppress(Exception):
224
+ load_dotenv(env_file, override=False)
225
+
226
+
227
+ def validate_required_env_keys() -> None:
228
+ """Ensure ENVIRONMENT_API_KEY (and optional Groq key) are set, prompting if needed."""
229
+
230
+ demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
231
+ env_file = demo_base / ".env"
232
+
233
+ if env_file.exists():
234
+ _load_dotenv_if_present(env_file)
235
+
236
+ env_api_key = os.environ.get("ENVIRONMENT_API_KEY", "").strip()
237
+ if not env_api_key:
238
+ env_api_key = click.prompt(
239
+ "Please enter your RL Environment API key",
240
+ type=str,
241
+ ).strip()
242
+ if not env_api_key:
243
+ raise click.ClickException("RL Environment API key is required to start the server")
244
+ os.environ["ENVIRONMENT_API_KEY"] = env_api_key
245
+ save_to_env_file(env_file, "ENVIRONMENT_API_KEY", env_api_key)
246
+
247
+ groq_api_key = os.environ.get("GROQ_API_KEY", "").strip()
248
+ if not groq_api_key:
249
+ click.echo("\nInference API key configuration:")
250
+ click.echo("This workflow requires a Groq API key.")
251
+ groq_api_key = click.prompt(
252
+ "Groq API key (or press Enter to skip)",
253
+ type=str,
254
+ default="",
255
+ show_default=False,
256
+ ).strip()
257
+ if groq_api_key:
258
+ os.environ["GROQ_API_KEY"] = groq_api_key
259
+ save_to_env_file(env_file, "GROQ_API_KEY", groq_api_key)
260
+
261
+
262
+ def print_demo_next_steps_if_applicable() -> None:
263
+ """Print helpful instructions when operating inside a demo directory."""
264
+
265
+ try:
266
+ from synth_ai.demos.demo_task_apps.core import load_demo_dir
267
+
268
+ cwd = Path.cwd().resolve()
269
+ demo_dir = load_demo_dir()
270
+
271
+ if demo_dir and Path(demo_dir).resolve() == cwd and (cwd / "run_local_rollout_traced.py").exists():
272
+ click.echo("\n" + "=" * 60)
273
+ click.echo("Next step: Collect traced rollouts")
274
+ click.echo("=" * 60)
275
+ click.echo("\nIn another terminal, run:")
276
+ click.echo(f" cd {cwd}")
277
+ click.echo(" uv run python run_local_rollout_traced.py")
278
+ click.echo("\nRun this 5-10 times to collect diverse traces.")
279
+ click.echo("=" * 60 + "\n")
280
+ except Exception:
281
+ pass
282
+
283
+
284
+ def _preview_secret(value: str) -> str:
285
+ if len(value) <= 10:
286
+ return value
287
+ return f"{value[:6]}...{value[-4:]}"
288
+
289
+
290
+ def preflight_env_key(env_paths: Sequence[Path] | None = None, *, crash_on_failure: bool = False) -> None:
291
+ """Ensure ENVIRONMENT_API_KEY exists and attempt to upload it to the backend."""
292
+
293
+ raw_backend = (
294
+ os.environ.get("BACKEND_BASE_URL")
295
+ or os.environ.get("SYNTH_BASE_URL")
296
+ or f"{PROD_BASE_URL_DEFAULT}/api"
297
+ )
298
+ backend_base = raw_backend.rstrip("/")
299
+ if not backend_base.endswith("/api"):
300
+ backend_base += "/api"
301
+
302
+ synth_key = os.environ.get("SYNTH_API_KEY") or ""
303
+ env_api_key = (
304
+ os.environ.get("ENVIRONMENT_API_KEY")
305
+ or os.environ.get("DEV_ENVIRONMENT_API_KEY")
306
+ or ""
307
+ ).strip()
308
+
309
+ def _mint_key() -> str | None:
310
+ try:
311
+ from synth_ai.learning.rl.secrets import mint_environment_api_key
312
+
313
+ key = mint_environment_api_key()
314
+ os.environ["ENVIRONMENT_API_KEY"] = key
315
+ os.environ.setdefault("DEV_ENVIRONMENT_API_KEY", key)
316
+ click.echo(f"[preflight] minted ENVIRONMENT_API_KEY ({_preview_secret(key)})")
317
+ return key
318
+ except Exception as exc: # pragma: no cover - defensive fallback
319
+ if crash_on_failure:
320
+ raise click.ClickException(
321
+ f"[CRITICAL] Failed to mint ENVIRONMENT_API_KEY: {exc}"
322
+ ) from exc
323
+ click.echo(
324
+ f"[WARN] Failed to mint ENVIRONMENT_API_KEY automatically ({exc}); proceeding without upload"
325
+ )
326
+ return None
327
+
328
+ minted = False
329
+ if not env_api_key:
330
+ env_api_key = _mint_key() or ""
331
+ minted = bool(env_api_key)
332
+
333
+ if env_api_key and minted and env_paths:
334
+ persist_env_api_key(env_api_key, env_paths)
335
+
336
+ if not synth_key.strip():
337
+ click.echo("[preflight] SYNTH_API_KEY not set; skipping backend preflight.")
338
+ return
339
+
340
+ if not env_api_key:
341
+ click.echo("[preflight] ENVIRONMENT_API_KEY missing; continuing without verification.")
342
+ return
343
+
344
+ try:
345
+ import base64
346
+
347
+ import httpx
348
+ from nacl.public import PublicKey, SealedBox
349
+ except Exception: # pragma: no cover - optional deps
350
+ click.echo("[preflight] Optional crypto dependencies missing; skipping upload.")
351
+ return
352
+
353
+ try:
354
+ with httpx.Client(timeout=15.0, headers={"Authorization": f"Bearer {synth_key}"}) as client:
355
+ click.echo(f"[preflight] backend={backend_base}")
356
+ click.echo("[preflight] fetching public key…")
357
+ rpk = client.get(f"{backend_base.rstrip('/')}/v1/crypto/public-key")
358
+ if rpk.status_code != 200:
359
+ click.echo(f"[preflight] public key fetch failed with {rpk.status_code}; skipping upload")
360
+ return
361
+ pk = (rpk.json() or {}).get("public_key")
362
+ if not pk:
363
+ click.echo("[preflight] no public key returned; skipping upload")
364
+ return
365
+
366
+ pk_bytes = base64.b64decode(pk, validate=True)
367
+ sealed_box = SealedBox(PublicKey(pk_bytes))
368
+ ciphertext = sealed_box.encrypt(env_api_key.encode("utf-8"))
369
+ ct_b64 = base64.b64encode(ciphertext).decode()
370
+ payload = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ct_b64}
371
+
372
+ click.echo(f"[preflight] posting to {backend_base.rstrip('/')}/v1/env-keys")
373
+ response = client.post(f"{backend_base.rstrip('/')}/v1/env-keys", json=payload)
374
+ if 200 <= response.status_code < 300:
375
+ click.echo(
376
+ f"✅ ENVIRONMENT_API_KEY uploaded successfully ({_preview_secret(env_api_key)})"
377
+ )
378
+ try:
379
+ ver = client.get(f"{backend_base.rstrip('/')}/v1/env-keys/verify")
380
+ if ver.status_code == 200 and (ver.json() or {}).get("present"):
381
+ click.echo("✅ Key verified in backend")
382
+ else:
383
+ click.echo(
384
+ f"⚠️ Verification returned {ver.status_code}, but upload succeeded - proceeding"
385
+ )
386
+ except Exception as verify_err: # pragma: no cover - verification optional
387
+ click.echo(
388
+ f"⚠️ Verification check failed ({verify_err}), but upload succeeded - proceeding"
389
+ )
390
+ return
391
+
392
+ snippet = response.text[:400] if response.text else ""
393
+ message = (
394
+ f"ENVIRONMENT_API_KEY upload failed with status {response.status_code}"
395
+ + (f" body={snippet}" if snippet else "")
396
+ )
397
+ if crash_on_failure:
398
+ raise click.ClickException(f"[CRITICAL] {message}")
399
+ click.echo(f"[WARN] {message}; proceeding anyway")
400
+ except Exception as exc: # pragma: no cover - network failures
401
+ message = f"Backend preflight for ENVIRONMENT_API_KEY failed: {exc}"
402
+ if crash_on_failure:
403
+ raise click.ClickException(f"[CRITICAL] {message}") from exc
404
+ click.echo(f"[WARN] {message}; proceeding anyway")
405
+
406
+
407
+ def load_env_values(paths: Sequence[Path], *, allow_empty: bool = False) -> dict[str, str]:
408
+ """Load values from a sequence of env files, returning a merged dictionary."""
409
+
410
+ values: dict[str, str] = {}
411
+ for path in paths:
412
+ try:
413
+ content = Path(path).read_text(encoding="utf-8")
414
+ except FileNotFoundError:
415
+ continue
416
+ for line in content.splitlines():
417
+ if not line or line.lstrip().startswith("#") or "=" not in line:
418
+ continue
419
+ key, value = line.split("=", 1)
420
+ key = key.strip()
421
+ value = value.strip()
422
+ if key and key not in values:
423
+ values[key] = value
424
+ if not allow_empty and not values:
425
+ raise click.ClickException("No environment values found")
426
+ os.environ.update({k: v for k, v in values.items() if k and v})
427
+ return values
428
+
429
+
430
+ def _parse_env_file(path: Path) -> dict[str, str]:
431
+ data: dict[str, str] = {}
432
+ try:
433
+ for line in path.read_text(encoding="utf-8").splitlines():
434
+ if not line or line.lstrip().startswith("#") or "=" not in line:
435
+ continue
436
+ key, value = line.split("=", 1)
437
+ data[key.strip()] = value.strip()
438
+ except FileNotFoundError:
439
+ pass
440
+ return data
441
+
442
+
443
+ def interactive_fill_env(env_path: Path) -> Path | None:
444
+ """Interactively collect credentials and write them to a .env file."""
445
+
446
+ existing = _parse_env_file(env_path) if env_path.exists() else {}
447
+
448
+ def _prompt(label: str, *, default: str = "", required: bool) -> str | None:
449
+ while True:
450
+ try:
451
+ value = click.prompt(
452
+ label,
453
+ default=default,
454
+ show_default=bool(default) or not required,
455
+ ).strip()
456
+ except (Abort, EOFError, KeyboardInterrupt):
457
+ click.echo("Aborted env creation.")
458
+ return None
459
+ if value or not required:
460
+ return value
461
+ click.echo("This field is required.")
462
+
463
+ env_default = existing.get("ENVIRONMENT_API_KEY", "").strip()
464
+ env_api_key = _prompt("ENVIRONMENT_API_KEY", default=env_default, required=True)
465
+ if env_api_key is None:
466
+ return None
467
+
468
+ synth_default = existing.get("SYNTH_API_KEY", "").strip()
469
+ openai_default = existing.get("OPENAI_API_KEY", "").strip()
470
+ synth_key = _prompt("SYNTH_API_KEY (optional)", default=synth_default, required=False) or ""
471
+ openai_key = _prompt("OPENAI_API_KEY (optional)", default=openai_default, required=False) or ""
472
+
473
+ env_path.parent.mkdir(parents=True, exist_ok=True)
474
+ env_path.write_text(
475
+ "\n".join(
476
+ [
477
+ f"ENVIRONMENT_API_KEY={env_api_key}",
478
+ f"SYNTH_API_KEY={synth_key}",
479
+ f"OPENAI_API_KEY={openai_key}",
480
+ ]
481
+ )
482
+ + "\n",
483
+ encoding="utf-8",
484
+ )
485
+ click.echo(f"Wrote credentials to {env_path}")
486
+ return env_path
487
+
488
+
489
+ def interactive_create_env(target_dir: Path) -> Path | None:
490
+ """Create a .env file for the provided directory if one does not exist."""
491
+
492
+ env_path = (target_dir / ".env").resolve()
493
+ if env_path.exists():
494
+ existing = _parse_env_file(env_path)
495
+ env_api = (existing.get("ENVIRONMENT_API_KEY") or "").strip()
496
+ if env_api:
497
+ return env_path
498
+ click.echo(f"Existing {env_path} is missing ENVIRONMENT_API_KEY. Let's update it.")
499
+ return interactive_fill_env(env_path)
500
+
501
+ click.echo("No .env found for this task app. Let's create one.")
502
+ return interactive_fill_env(env_path)
503
+
504
+
505
+ def ensure_env_values(env_paths: list[Path], fallback_dir: Path) -> None:
506
+ """Ensure required env values are present, prompting to create .env if needed."""
507
+
508
+ if (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
509
+ return
510
+
511
+ target = env_paths[0] if env_paths else (fallback_dir / ".env").resolve()
512
+ result = interactive_fill_env(target)
513
+ if result is None:
514
+ raise click.ClickException("ENVIRONMENT_API_KEY required to continue")
515
+
516
+ load_env_values([result])
517
+ if not (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
518
+ raise click.ClickException("Failed to load ENVIRONMENT_API_KEY from generated .env")
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ModalServeCliError
5
+ from .validation import validate_modal_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ModalServeCliError",
11
+ "validate_modal_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("modal-serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("modal-serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ModalServeCliError(RuntimeError):
5
+ """Base exception for modal-serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ModalServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_modal_serve_options"]
7
+
8
+
9
+ def validate_modal_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the modal-serve CLI command."""
11
+ return options
synth_ai/cli/recent.py CHANGED
@@ -12,12 +12,13 @@ from rich import box
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
- from synth_ai.cli._storage import load_storage
15
+ from ._storage import load_storage
16
16
 
17
17
  if TYPE_CHECKING: # pragma: no cover - typing only
18
18
  import pandas as pd
19
19
  else:
20
20
  pd = Any # type: ignore[assignment]
21
+
21
22
  def _fmt_int(v: Any) -> str:
22
23
  try:
23
24
  return f"{int(v):,}"
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ServeCliError
5
+ from .validation import validate_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ServeCliError",
11
+ "validate_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("Serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ServeCliError(RuntimeError):
5
+ """Base exception for serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_serve_options"]
7
+
8
+
9
+ def validate_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the serve CLI command."""
11
+ return options
synth_ai/cli/setup.py ADDED
@@ -0,0 +1,21 @@
1
+ """Instructions on Docs → https://usesynth.ai/cli-cmds/setup"""
2
+
3
+ import click
4
+ from synth_ai.auth.credentials import fetch_credentials_from_web_browser_session
5
+
6
+
7
+ @click.command("setup")
8
+ @click.option(
9
+ "--local",
10
+ is_flag=True,
11
+ help="Load your credentials from your local machine"
12
+ )
13
+ @click.option(
14
+ "--dev",
15
+ is_flag=True
16
+ )
17
+ def setup_cmd(local: bool, dev: bool) -> None:
18
+ fetch_credentials_from_web_browser_session(
19
+ browser=not local,
20
+ prod=not dev
21
+ )