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,308 @@
1
+ import contextlib
2
+ import json
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from urllib.parse import urlparse, urlunparse
9
+
10
+ from synth_ai.demos import core as demo_core
11
+ from synth_ai.demos.core import DEFAULT_TASK_APP_SECRET_NAME, DemoEnv
12
+
13
+ from .env import mask_str
14
+ from .http import http_request
15
+ from .process import popen_capture
16
+ from .user_config import load_user_config
17
+
18
+ __all__ = [
19
+ "ensure_modal_installed",
20
+ "ensure_task_app_ready",
21
+ "find_asgi_apps",
22
+ "is_local_demo_url",
23
+ "is_modal_public_url",
24
+ "normalize_endpoint_url",
25
+ ]
26
+
27
+
28
+ def is_modal_public_url(url: str | None) -> bool:
29
+ try:
30
+ candidate = (url or "").strip().lower()
31
+ if not candidate or not (candidate.startswith("http://") or candidate.startswith("https://")):
32
+ return False
33
+ return (".modal.run" in candidate) and ("modal.local" not in candidate) and ("pypi-mirror" not in candidate)
34
+ except Exception:
35
+ return False
36
+
37
+
38
+ def is_local_demo_url(url: str | None) -> bool:
39
+ try:
40
+ candidate = (url or "").strip().lower()
41
+ if not candidate:
42
+ return False
43
+ return candidate.startswith("http://127.0.0.1") or candidate.startswith("http://localhost")
44
+ except Exception:
45
+ return False
46
+
47
+
48
+ def normalize_endpoint_url(url: str) -> str:
49
+ """Convert loopback URLs to forms accepted by the backend."""
50
+ if not url:
51
+ return url
52
+ try:
53
+ parsed = urlparse(url)
54
+ host = parsed.hostname or ""
55
+ if host in {"127.0.0.1", "::1"}:
56
+ new_host = "localhost"
57
+ netloc = new_host
58
+ if parsed.port:
59
+ netloc = f"{new_host}:{parsed.port}"
60
+ if parsed.username:
61
+ creds = parsed.username
62
+ if parsed.password:
63
+ creds += f":{parsed.password}"
64
+ netloc = f"{creds}@{netloc}"
65
+ parsed = parsed._replace(netloc=netloc)
66
+ return urlunparse(parsed)
67
+ except Exception:
68
+ pass
69
+ return url
70
+
71
+
72
+ def find_asgi_apps(root: Path) -> list[Path]:
73
+ """Recursively search for Python files that declare a Modal ASGI app."""
74
+ results: list[Path] = []
75
+ skip_dirs = {
76
+ ".git",
77
+ ".hg",
78
+ ".svn",
79
+ "node_modules",
80
+ "dist",
81
+ "build",
82
+ "__pycache__",
83
+ ".ruff_cache",
84
+ ".mypy_cache",
85
+ "venv",
86
+ ".venv",
87
+ }
88
+ for dirpath, dirnames, filenames in os.walk(root):
89
+ dirnames[:] = [d for d in dirnames if d not in skip_dirs]
90
+ for name in filenames:
91
+ if not name.endswith(".py"):
92
+ continue
93
+ path = Path(dirpath) / name
94
+ try:
95
+ with path.open("r", encoding="utf-8", errors="ignore") as fh:
96
+ txt = fh.read()
97
+ if ("@asgi_app()" in txt) or ("@modal.asgi_app()" in txt):
98
+ results.append(path)
99
+ except Exception:
100
+ continue
101
+
102
+ def _priority(path: Path) -> tuple[int, str]:
103
+ rel = str(path.resolve())
104
+ in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
105
+ return (0 if in_demo else 1, rel)
106
+
107
+ results.sort(key=_priority)
108
+ return results
109
+
110
+
111
+ def ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoEnv:
112
+ persist_path = demo_core.load_demo_dir() or os.getcwd()
113
+ user_config_map = load_user_config()
114
+
115
+ env_key = (env.env_api_key or "").strip()
116
+ if not env_key:
117
+ raise RuntimeError(
118
+ f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai demo deploy` first."
119
+ )
120
+
121
+ template_id = demo_core.load_template_id()
122
+ allow_local = template_id == "crafter-local"
123
+
124
+ task_url = env.task_app_base_url
125
+ url_ok = is_modal_public_url(task_url) or (allow_local and is_local_demo_url(task_url or ""))
126
+ if not task_url or not url_ok:
127
+ resolved = task_url or ""
128
+ dynamic_lookup_allowed = env.task_app_name and not (
129
+ allow_local and is_local_demo_url(task_url or "")
130
+ )
131
+ if dynamic_lookup_allowed and not is_modal_public_url(resolved):
132
+ code, out = popen_capture(
133
+ [
134
+ "uv",
135
+ "run",
136
+ "python",
137
+ "-m",
138
+ "modal",
139
+ "app",
140
+ "url",
141
+ env.task_app_name,
142
+ ]
143
+ )
144
+ if code == 0 and out:
145
+ for token in out.split():
146
+ if is_modal_public_url(token):
147
+ resolved = token.strip().rstrip("/")
148
+ break
149
+ if dynamic_lookup_allowed and not is_modal_public_url(resolved):
150
+ try:
151
+ choice = (
152
+ input(
153
+ f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
154
+ ).strip().lower()
155
+ or "y"
156
+ )
157
+ except Exception:
158
+ choice = "y"
159
+ if choice.startswith("y"):
160
+ code, out = popen_capture(
161
+ [
162
+ "uv",
163
+ "run",
164
+ "python",
165
+ "-m",
166
+ "modal",
167
+ "app",
168
+ "url",
169
+ env.task_app_name,
170
+ ]
171
+ )
172
+ if code == 0 and out:
173
+ for token in out.split():
174
+ if is_modal_public_url(token):
175
+ resolved = token.strip().rstrip("/")
176
+ break
177
+ if not is_modal_public_url(resolved):
178
+ hint = "Examples: https://<app-name>-fastapi-app.modal.run"
179
+ if allow_local:
180
+ hint += " or http://127.0.0.1:8001"
181
+ print(f"[{label}] Task app URL not configured or not a valid target.")
182
+ print(hint)
183
+ entered = input(
184
+ "Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
185
+ ).strip()
186
+ if not entered:
187
+ raise RuntimeError(f"[{label}] Task App URL is required.")
188
+ entered_clean = entered.rstrip("/")
189
+ if not (
190
+ is_modal_public_url(entered_clean)
191
+ or (allow_local and is_local_demo_url(entered_clean))
192
+ ):
193
+ raise RuntimeError(f"[{label}] Valid Task App URL is required.")
194
+ task_url = entered_clean
195
+ else:
196
+ task_url = resolved
197
+ demo_core.persist_task_url(task_url, name=(env.task_app_name or None), path=persist_path)
198
+
199
+ app_name = (env.task_app_name or "").strip()
200
+ requires_modal_name = is_modal_public_url(task_url)
201
+ if requires_modal_name and not app_name:
202
+ fallback = input("Enter Modal app name for the task app (required): ").strip()
203
+ if not fallback:
204
+ raise RuntimeError(f"[{label}] Task app name is required.")
205
+ app_name = fallback
206
+ demo_core.persist_task_url(task_url, name=app_name, path=persist_path)
207
+
208
+ demo_core.persist_task_url(task_url, name=app_name if requires_modal_name else None, path=persist_path)
209
+ if synth_key:
210
+ os.environ["SYNTH_API_KEY"] = synth_key
211
+
212
+ openai_key = (
213
+ os.environ.get("OPENAI_API_KEY")
214
+ or str(user_config_map.get("OPENAI_API_KEY") or "")
215
+ ).strip()
216
+ if openai_key:
217
+ os.environ["OPENAI_API_KEY"] = openai_key
218
+
219
+ print(f"[{label}] Verifying rollout health:")
220
+ try:
221
+ preview = mask_str(env_key)
222
+ print(f"[{label}] {preview}")
223
+ except Exception:
224
+ pass
225
+ health_base = task_url.rstrip("/")
226
+ health_urls = [f"{health_base}/health/rollout", f"{health_base}/health"]
227
+ rc = 0
228
+ body: Any = ""
229
+ for h in health_urls:
230
+ print(f"[{label}] GET", h)
231
+ rc, body = http_request("GET", h, headers={"X-API-Key": env_key})
232
+ if rc == 200:
233
+ break
234
+ print(f"[{label}] status: {rc}")
235
+ try:
236
+ preview_body = json.dumps(body)[:800] if isinstance(body, dict) else str(body)[:800]
237
+ except Exception:
238
+ preview_body = str(body)[:800]
239
+ print(f"[{label}] body:", preview_body)
240
+ if rc != 200:
241
+ print(f"[{label}] Warning: rollout health check failed ({rc}). Response: {body}")
242
+ with contextlib.suppress(Exception):
243
+ print(f"[{label}] Sent header X-API-Key → {mask_str(env_key)}")
244
+ else:
245
+ print(f"[{label}] Task app rollout health check OK.")
246
+
247
+ os.environ["TASK_APP_BASE_URL"] = task_url
248
+ os.environ["ENVIRONMENT_API_KEY"] = env_key
249
+ os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
250
+ updated_env = demo_core.load_env()
251
+ updated_env.env_api_key = env_key
252
+ updated_env.task_app_base_url = task_url
253
+ updated_env.task_app_name = app_name if requires_modal_name else ""
254
+ updated_env.task_app_secret_name = DEFAULT_TASK_APP_SECRET_NAME
255
+ return updated_env
256
+
257
+
258
+ def ensure_modal_installed() -> None:
259
+ """Install the modal package if it is not already available and check authentication."""
260
+ modal_installed = False
261
+ try:
262
+ import importlib.util as import_util
263
+
264
+ if import_util.find_spec("modal") is not None:
265
+ modal_installed = True
266
+ except Exception:
267
+ pass
268
+
269
+ if not modal_installed:
270
+ print("modal not found; installing…")
271
+ try:
272
+ if shutil.which("uv"):
273
+ code, out = popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
274
+ else:
275
+ code, out = popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
276
+ if code != 0:
277
+ print(out)
278
+ print("Failed to install modal; continuing may fail.")
279
+ return
280
+ print("✓ modal installed successfully")
281
+ modal_installed = True
282
+ except Exception as exc:
283
+ print(f"modal install error: {exc}")
284
+ return
285
+
286
+ if modal_installed:
287
+ try:
288
+ import importlib.util as import_util
289
+
290
+ if import_util.find_spec("modal") is None:
291
+ print("Warning: modal is still not importable after install attempt.")
292
+ return
293
+ except Exception:
294
+ print("Warning: unable to verify modal installation.")
295
+ return
296
+
297
+ auth_ok, auth_msg = demo_core.modal_auth_status()
298
+ if auth_ok:
299
+ print(f"✓ Modal authenticated: {auth_msg}")
300
+ else:
301
+ print("\n⚠️ Modal authentication required")
302
+ print(f" Status: {auth_msg}")
303
+ print("\n To authenticate Modal, run:")
304
+ print(" modal setup")
305
+ print("\n Or set environment variables:")
306
+ print(" export MODAL_TOKEN_ID=your-token-id")
307
+ print(" export MODAL_TOKEN_SECRET=your-token-secret")
308
+ print("\n You can deploy later after authenticating.\n")
@@ -0,0 +1,212 @@
1
+ import os
2
+ import signal
3
+ import socket
4
+ import time
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ __all__ = [
9
+ "ensure_local_port_available",
10
+ "popen_capture",
11
+ "popen_stream",
12
+ "popen_stream_capture",
13
+ ]
14
+
15
+
16
+ def popen_capture(
17
+ cmd: list[str], cwd: str | None = None, env: dict[str, Any] | None = None
18
+ ) -> tuple[int, str]:
19
+ """Execute a subprocess and capture combined stdout/stderr."""
20
+ import subprocess
21
+
22
+ try:
23
+ proc = subprocess.Popen(
24
+ cmd,
25
+ cwd=cwd,
26
+ env=env,
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.STDOUT,
29
+ text=True,
30
+ )
31
+ out, _ = proc.communicate()
32
+ return int(proc.returncode or 0), out or ""
33
+ except Exception as exc:
34
+ return 1, str(exc)
35
+
36
+
37
+ def popen_stream(
38
+ cmd: list[str], cwd: str | None = None, env: dict[str, Any] | None = None
39
+ ) -> int:
40
+ """Stream subprocess output line-by-line to stdout for real-time feedback."""
41
+ import subprocess
42
+ import threading
43
+
44
+ try:
45
+ proc = subprocess.Popen(
46
+ cmd,
47
+ cwd=cwd,
48
+ env=env,
49
+ stdout=subprocess.PIPE,
50
+ stderr=subprocess.STDOUT,
51
+ text=True,
52
+ bufsize=1,
53
+ )
54
+ except Exception as exc:
55
+ print(f"Failed to launch {' '.join(cmd)}: {exc}")
56
+ return 1
57
+
58
+ def _pump(stdout) -> None:
59
+ try:
60
+ for line in stdout:
61
+ print(line.rstrip())
62
+ except Exception:
63
+ pass
64
+
65
+ if proc.stdout is not None:
66
+ t = threading.Thread(target=_pump, args=(proc.stdout,), daemon=True)
67
+ t.start()
68
+ proc.wait()
69
+ t.join(timeout=1.0)
70
+ else:
71
+ proc.wait()
72
+ return int(proc.returncode or 0)
73
+
74
+
75
+ def popen_stream_capture(
76
+ cmd: list[str], cwd: str | None = None, env: dict[str, Any] | None = None
77
+ ) -> tuple[int, str]:
78
+ """Stream subprocess output to stdout and also capture it into a buffer."""
79
+ import subprocess
80
+ import threading
81
+
82
+ buf_lines: list[str] = []
83
+ try:
84
+ proc = subprocess.Popen(
85
+ cmd,
86
+ cwd=cwd,
87
+ env=env,
88
+ stdout=subprocess.PIPE,
89
+ stderr=subprocess.STDOUT,
90
+ text=True,
91
+ bufsize=1,
92
+ )
93
+ except Exception as exc:
94
+ print(f"Failed to launch {' '.join(cmd)}: {exc}")
95
+ return 1, ""
96
+
97
+ def _pump(stdout) -> None:
98
+ try:
99
+ for line in stdout:
100
+ line = line.rstrip()
101
+ print(line)
102
+ buf_lines.append(line)
103
+ except Exception:
104
+ pass
105
+
106
+ if proc.stdout is not None:
107
+ t = threading.Thread(target=_pump, args=(proc.stdout,), daemon=True)
108
+ t.start()
109
+ proc.wait()
110
+ t.join(timeout=1.0)
111
+ else:
112
+ proc.wait()
113
+ return int(proc.returncode or 0), "\n".join(buf_lines)
114
+
115
+
116
+ def _list_process_ids(port: int) -> list[int]:
117
+ try:
118
+ import subprocess
119
+
120
+ out = subprocess.run(
121
+ ["lsof", "-ti", f"TCP:{port}"],
122
+ capture_output=True,
123
+ text=True,
124
+ check=False,
125
+ )
126
+ if not out.stdout:
127
+ return []
128
+ result: list[int] = []
129
+ for token in out.stdout.strip().splitlines():
130
+ token = token.strip()
131
+ if token.isdigit():
132
+ result.append(int(token))
133
+ return result
134
+ except Exception:
135
+ return []
136
+
137
+
138
+ def _terminate_pids(pids: Iterable[int], *, aggressive: bool) -> bool:
139
+ terminated_any = False
140
+ for pid in pids:
141
+ try:
142
+ os.kill(pid, signal.SIGTERM)
143
+ terminated_any = True
144
+ except Exception as exc:
145
+ print(f"Failed to terminate PID {pid}: {exc}")
146
+ if terminated_any:
147
+ time.sleep(1.0)
148
+
149
+ if aggressive and pids:
150
+ still_running = []
151
+ for pid in pids:
152
+ try:
153
+ os.kill(pid, 0)
154
+ except OSError:
155
+ continue
156
+ still_running.append(pid)
157
+ if still_running:
158
+ for pid in still_running:
159
+ try:
160
+ os.kill(pid, signal.SIGKILL)
161
+ except Exception as exc:
162
+ print(f"Failed to force terminate PID {pid}: {exc}")
163
+ time.sleep(0.5)
164
+ return terminated_any
165
+
166
+
167
+ def ensure_local_port_available(host: str, port: int, *, force: bool = False) -> bool:
168
+ """Ensure ``host:port`` is free before starting a local server."""
169
+
170
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
171
+ sock.settimeout(0.5)
172
+ in_use = sock.connect_ex((host, port)) == 0
173
+ if not in_use:
174
+ return True
175
+
176
+ print(f"Port {port} on {host} is already in use.")
177
+ pids = _list_process_ids(port)
178
+
179
+ if pids:
180
+ print("Found processes using this port:")
181
+ for pid in pids:
182
+ print(f" PID {pid}")
183
+ else:
184
+ print("Could not automatically identify the owning process.")
185
+
186
+ if not force:
187
+ try:
188
+ choice = input(f"Stop the existing process on port {port}? [y/N]: ").strip().lower() or "n"
189
+ except Exception:
190
+ choice = "n"
191
+ if not choice.startswith("y"):
192
+ print("Aborting; stop the running server and try again.")
193
+ return False
194
+ else:
195
+ print("Attempting to terminate the existing process...")
196
+
197
+ if pids:
198
+ _terminate_pids(pids, aggressive=force)
199
+ else:
200
+ print("Unable to determine owning process. Please stop it manually and retry.")
201
+ return False
202
+
203
+ for _ in range(10):
204
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
205
+ sock.settimeout(0.5)
206
+ if sock.connect_ex((host, port)) != 0:
207
+ print("Port is now available.")
208
+ return True
209
+ time.sleep(0.5)
210
+
211
+ print("Port still in use after terminating processes.")
212
+ return False
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import Namespace
4
+ from collections.abc import Callable, Mapping, MutableMapping
5
+ from typing import Any
6
+
7
+ __all__ = ["ensure_required_args"]
8
+
9
+
10
+ def ensure_required_args(
11
+ args: Namespace,
12
+ prompts: Mapping[str, str],
13
+ *,
14
+ coerce: Mapping[str, Callable[[Any], Any]] | None = None,
15
+ defaults: Mapping[str, Any] | None = None,
16
+ ) -> Namespace:
17
+ """Ensure required CLI arguments are populated.
18
+
19
+ Legacy helper that historically prompted users. Our tests rely on it to
20
+ populate defaults and perform simple coercions, so we implement a minimal,
21
+ non-interactive version that mirrors that behaviour.
22
+ """
23
+
24
+ coerce_map = dict(coerce or {})
25
+ default_map: MutableMapping[str, Any] = dict(defaults or {})
26
+
27
+ for key, label in prompts.items():
28
+ value = getattr(args, key, None)
29
+ if value in (None, "") and key in default_map:
30
+ value = default_map[key]
31
+ if value in (None, ""):
32
+ raise ValueError(f"{label} is required")
33
+ if key in coerce_map:
34
+ try:
35
+ value = coerce_map[key](value)
36
+ except Exception as exc: # pragma: no cover - defensive
37
+ raise ValueError(f"Failed to normalize {label}: {exc}") from exc
38
+ setattr(args, key, value)
39
+ return args
synth_ai/utils/sqld.py ADDED
@@ -0,0 +1,122 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import socket
6
+ import subprocess
7
+ import tempfile
8
+
9
+ import click
10
+
11
+ SQLD_VERSION = "v0.26.2"
12
+
13
+
14
+ def find_sqld_binary() -> str | None:
15
+ """Locate an existing sqld binary on PATH or in common install locations."""
16
+
17
+ sqld_path = shutil.which("sqld")
18
+ if sqld_path:
19
+ return sqld_path
20
+ common_paths = [
21
+ "/usr/local/bin/sqld",
22
+ "/usr/bin/sqld",
23
+ os.path.expanduser("~/.local/bin/sqld"),
24
+ os.path.expanduser("~/bin/sqld"),
25
+ os.path.expanduser("~/.turso/bin/sqld"),
26
+ ]
27
+ for path in common_paths:
28
+ if os.path.exists(path) and os.access(path, os.X_OK):
29
+ return path
30
+ return None
31
+
32
+
33
+ def install_sqld() -> str:
34
+ """Install sqld via the Turso CLI, installing the CLI via Homebrew if needed."""
35
+
36
+ click.echo("🔧 sqld not found. Attempting automatic install...")
37
+
38
+ turso_cli_path = shutil.which("turso")
39
+ brew_path = shutil.which("brew")
40
+
41
+ if not turso_cli_path:
42
+ if not brew_path:
43
+ raise click.ClickException(
44
+ "Automatic install requires either Homebrew or an existing Turso CLI.\n"
45
+ "Install manually using one of:\n"
46
+ " • brew install tursodatabase/tap/turso\n"
47
+ " • curl -sSfL https://get.tur.so/install.sh | bash\n"
48
+ "Then run 'turso dev' once and re-run this command."
49
+ )
50
+
51
+ click.echo("🧰 Installing Turso CLI via Homebrew (tursodatabase/tap/turso)…")
52
+ try:
53
+ subprocess.run(
54
+ [brew_path, "install", "tursodatabase/tap/turso"],
55
+ check=True,
56
+ )
57
+ except subprocess.CalledProcessError as exc:
58
+ raise click.ClickException(
59
+ "Homebrew install failed. Please resolve brew errors and retry."
60
+ ) from exc
61
+
62
+ turso_cli_path = shutil.which("turso")
63
+ if not turso_cli_path:
64
+ raise click.ClickException(
65
+ "Homebrew reported success but the 'turso' binary is not on PATH."
66
+ )
67
+
68
+ click.echo("📥 Downloading sqld via 'turso dev' (this may take a few seconds)…")
69
+
70
+ with tempfile.NamedTemporaryFile(prefix="synth_sqld_", suffix=".db", delete=False) as temp_db:
71
+ temp_db_path = temp_db.name
72
+
73
+ env = os.environ.copy()
74
+ env.setdefault("TURSO_NONINTERACTIVE", "1")
75
+
76
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
77
+ sock.bind(("127.0.0.1", 0))
78
+ port = sock.getsockname()[1]
79
+
80
+ cmd = [
81
+ turso_cli_path,
82
+ "dev",
83
+ f"--db-file={temp_db_path}",
84
+ f"--port={port}",
85
+ ]
86
+ proc: subprocess.Popen[str] | None = None
87
+ stdout_data = ""
88
+ stderr_data = ""
89
+ try:
90
+ proc = subprocess.Popen(
91
+ cmd,
92
+ stdout=subprocess.PIPE,
93
+ stderr=subprocess.PIPE,
94
+ text=True,
95
+ env=env,
96
+ )
97
+ try:
98
+ stdout_data, stderr_data = proc.communicate(timeout=10)
99
+ except subprocess.TimeoutExpired:
100
+ proc.terminate()
101
+ try:
102
+ stdout_data, stderr_data = proc.communicate(timeout=5)
103
+ except subprocess.TimeoutExpired:
104
+ proc.kill()
105
+ stdout_data, stderr_data = proc.communicate()
106
+ finally:
107
+ if proc and proc.returncode not in (0, None) and (stdout_data or stderr_data):
108
+ logging.getLogger(__name__).debug(
109
+ "turso dev stdout: %s\nstderr: %s", stdout_data, stderr_data
110
+ )
111
+ with contextlib.suppress(OSError):
112
+ os.unlink(temp_db_path)
113
+
114
+ sqld_path = find_sqld_binary()
115
+ if sqld_path:
116
+ click.echo(f"✅ sqld available at {sqld_path}")
117
+ return sqld_path
118
+
119
+ raise click.ClickException(
120
+ "sqld download did not succeed. Run 'turso dev' manually once, "
121
+ "ensure it downloads sqld, and try again."
122
+ )