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
synth_ai/utils/cli.py ADDED
@@ -0,0 +1,131 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any, cast
3
+
4
+ import click
5
+
6
+
7
+ class PromptedChoiceType(click.Choice):
8
+ """`click.Choice` variant that reprompts with an interactive menu on failure.
9
+
10
+ Example
11
+ -------
12
+ ```python
13
+ import click
14
+
15
+ from synth_ai.utils.cli import PromptedChoiceType, PromptedChoiceOption
16
+
17
+
18
+ @click.command()
19
+ @click.option(
20
+ "--mode",
21
+ cls=PromptedChoiceOption,
22
+ type=PromptedChoiceType(["sft", "rl"]),
23
+ required=True,
24
+ )
25
+ def train(mode: str) -> None:
26
+ click.echo(f"Selected mode: {mode}")
27
+ ```
28
+ """
29
+
30
+ def convert(
31
+ self,
32
+ value: Any,
33
+ param: click.Parameter | None,
34
+ ctx: click.Context | None,
35
+ ) -> str:
36
+ """Validate *value*; prompt for a replacement when it is missing or invalid."""
37
+ if param is None:
38
+ raise RuntimeError("Invalid parameter")
39
+ if ctx is None:
40
+ raise RuntimeError("Invalid context")
41
+ if value in (None, ""):
42
+ return self._prompt_user(param, ctx)
43
+ try:
44
+ return super().convert(value, param, ctx)
45
+ except click.BadParameter:
46
+ cmd_name = self._get_cmd_name(ctx)
47
+ if getattr(param, "opts", None):
48
+ click.echo(f'\n[{cmd_name}] Invalid value "{value}" for {self._get_arg_name(param)}')
49
+ else:
50
+ click.echo(f'\n[{cmd_name}] Invalid value "{value}"')
51
+ return self._prompt_user(param, ctx)
52
+
53
+ def _prompt_user(
54
+ self,
55
+ param: click.Parameter,
56
+ ctx: click.Context | None,
57
+ ) -> str:
58
+ """Render a numbered picker and return the user’s selection."""
59
+ arg_name = self._get_arg_name(param)
60
+ click.echo(f"\n[{self._get_cmd_name(ctx)}] Please choose a value for {arg_name}")
61
+ for index, choice in enumerate(self.choices, 1):
62
+ click.echo(f" [{index}] {choice}")
63
+ while True:
64
+ selection = click.prompt("> ", type=int)
65
+ if 1 <= selection <= len(self.choices):
66
+ return cast(str, self.choices[selection - 1])
67
+ click.echo(f"Invalid selection for {arg_name}, please try again")
68
+
69
+ def _get_cmd_name(self, ctx: click.Context | None) -> str:
70
+ """Safely extract the current command name for diagnostic output."""
71
+ cmd = getattr(ctx, "command", None) if ctx is not None else None
72
+ if cmd is None:
73
+ return "?"
74
+ name = getattr(cmd, "name", None)
75
+ return name or "?"
76
+
77
+ def _get_arg_name(self, param: click.Parameter) -> str:
78
+ """Return the most human-friendly identifier for the parameter."""
79
+ opts = getattr(param, "opts", None)
80
+ if opts:
81
+ return opts[-1]
82
+ name = getattr(param, "name", None)
83
+ if name:
84
+ return name
85
+ human_name = getattr(param, "human_readable_name", None)
86
+ if human_name:
87
+ return human_name
88
+ return "?"
89
+
90
+
91
+ class PromptedChoiceOption(click.Option):
92
+ """`click.Option` subclass that triggers the interactive picker when missing.
93
+
94
+ Example
95
+ -------
96
+ ```python
97
+ import click
98
+
99
+ from synth_ai.utils.cli import PromptedChoiceType, PromptedChoiceOption
100
+
101
+
102
+ @click.command()
103
+ @click.option(
104
+ "--mode",
105
+ cls=PromptedChoiceOption,
106
+ type=PromptedChoiceType(["sft", "rl"]),
107
+ required=True,
108
+ )
109
+ def train(mode: str) -> None:
110
+ click.echo(f"Selected mode: {mode}")
111
+ ```
112
+ """
113
+
114
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
115
+ kwargs.setdefault("prompt", True)
116
+ kwargs.setdefault("prompt_required", True)
117
+ super().__init__(*args, **kwargs)
118
+
119
+ def prompt_for_value(self, ctx: click.Context) -> Any:
120
+ """Invoke the choice picker when the option was omitted."""
121
+ option_type = getattr(self, "type", None)
122
+ if isinstance(option_type, PromptedChoiceType):
123
+ return option_type._prompt_user(self, ctx)
124
+ return super().prompt_for_value(ctx)
125
+
126
+
127
+ def print_next_step(message: str, lines: Sequence[str]) -> None:
128
+ print(f"\n➡️ Next, {message}:")
129
+ for line in lines:
130
+ print(f" {line}")
131
+ print("")
synth_ai/utils/env.py ADDED
@@ -0,0 +1,294 @@
1
+ import json
2
+ import os
3
+ import string
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ _ENV_SAFE_CHARS = set(string.ascii_letters + string.digits + "_-./:@+=")
9
+
10
+
11
+ def _format_env_value(value: str) -> str:
12
+ if value == "":
13
+ return '""'
14
+ if all(char in _ENV_SAFE_CHARS for char in value):
15
+ return value
16
+ return json.dumps(value)
17
+
18
+
19
+ def _strip_inline_comment(value: str) -> str:
20
+ in_single = False
21
+ in_double = False
22
+ escaped = False
23
+ for idx, char in enumerate(value):
24
+ if escaped:
25
+ escaped = False
26
+ continue
27
+ if char == "\\":
28
+ escaped = True
29
+ continue
30
+ if char == "'" and not in_double:
31
+ in_single = not in_single
32
+ continue
33
+ if char == '"' and not in_single:
34
+ in_double = not in_double
35
+ continue
36
+ if char == '#' and not in_single and not in_double:
37
+ return value[:idx].rstrip()
38
+ return value.rstrip()
39
+
40
+
41
+ def _parse_env_assignment(line: str) -> tuple[str, str] | None:
42
+ stripped = line.strip()
43
+ if not stripped or stripped.startswith('#'):
44
+ return None
45
+ if stripped.lower().startswith("export "):
46
+ stripped = stripped[7:].lstrip()
47
+ if '=' not in stripped:
48
+ return None
49
+ key_part, value_part = stripped.split('=', 1)
50
+ key = key_part.strip()
51
+ if not key:
52
+ return None
53
+ value_candidate = _strip_inline_comment(value_part.strip())
54
+ if not value_candidate:
55
+ return key, ""
56
+ if (
57
+ len(value_candidate) >= 2
58
+ and value_candidate[0] in {'"', "'"}
59
+ and value_candidate[-1] == value_candidate[0]
60
+ ):
61
+ value = value_candidate[1:-1]
62
+ else:
63
+ value = value_candidate
64
+ return key, value
65
+
66
+
67
+ def _prompt_manual_env_value(key: str) -> str:
68
+ while True:
69
+ value = click.prompt(
70
+ f"Enter value for {key}",
71
+ hide_input=False,
72
+ default="",
73
+ show_default=False,
74
+ type=str,
75
+ ).strip()
76
+ if value:
77
+ return value
78
+ if click.confirm("Save empty value?", default=False):
79
+ return ""
80
+ click.echo("Empty value discarded; enter a value or confirm empty to continue")
81
+
82
+
83
+ def mask_str(input: str, position: int = 3) -> str:
84
+ return input[:position] + "..." + input[-position:] if len(input) > position * 2 else "***"
85
+
86
+
87
+ def get_env_file_paths(base_dir: str | Path = '.') -> list[Path]:
88
+ base = Path(base_dir).resolve()
89
+ return [path for path in base.rglob(".env*") if path.is_file()]
90
+
91
+
92
+ def get_synth_config_file_paths() -> list[Path]:
93
+ dir = Path.home() / ".synth-ai"
94
+ if not dir.exists():
95
+ return []
96
+ return [path for path in dir.glob("*.json") if path.is_file()]
97
+
98
+
99
+ def filter_env_files_by_key(key: str, paths: list[Path]) -> list[tuple[Path, str]]:
100
+ matches: list[tuple[Path, str]] = []
101
+ for path in paths:
102
+ try:
103
+ with path.open('r', encoding="utf-8") as file:
104
+ for line in file:
105
+ parsed = _parse_env_assignment(line)
106
+ if parsed is None:
107
+ continue
108
+ parsed_key, value = parsed
109
+ if parsed_key == key:
110
+ matches.append((path, value))
111
+ break
112
+ except (OSError, UnicodeDecodeError):
113
+ continue
114
+ return matches
115
+
116
+
117
+ def filter_json_files_by_key(key: str, paths: list[Path]) -> list[tuple[Path, str]]:
118
+ matches: list[tuple[Path, str]] = []
119
+ for path in paths:
120
+ try:
121
+ with path.open('r', encoding="utf-8") as file:
122
+ data = json.load(file)
123
+ if key in data and isinstance(data[key], str):
124
+ matches.append((path, data[key]))
125
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
126
+ continue
127
+ return matches
128
+
129
+
130
+ def ensure_env_var(key: str, expected_value: str) -> None:
131
+ actual_value = os.getenv(key)
132
+ if expected_value != actual_value:
133
+ raise ValueError(f"Expected: {key}={expected_value}\nActual: {key}={actual_value}")
134
+
135
+
136
+ def resolve_env_var(key: str) -> str:
137
+ env_value = os.getenv(key)
138
+ if env_value is not None:
139
+ click.echo(f"Using {key}={mask_str(env_value)} from process environment")
140
+ return env_value
141
+
142
+ value: str = ""
143
+
144
+ env_file_paths = filter_env_files_by_key(key, get_env_file_paths())
145
+ synth_file_paths = filter_json_files_by_key(key, get_synth_config_file_paths())
146
+
147
+ options: list[tuple[str, str]] = []
148
+ for path, value in env_file_paths:
149
+ resolved_path = path.resolve()
150
+ try:
151
+ rel_path = str(resolved_path.relative_to(Path.cwd()))
152
+ except ValueError:
153
+ rel_path = str(resolved_path)
154
+ label = f"({rel_path}) {mask_str(value)}"
155
+ options.append((label, value))
156
+ for path, value in synth_file_paths:
157
+ label = f"({path}) {mask_str(value)}"
158
+ options.append((label, value))
159
+
160
+ if options:
161
+ click.echo(f"\nFound the following options for {key}")
162
+ for i, (label, _) in enumerate(options, start=1):
163
+ click.echo(f" [{i}] {label}")
164
+ click.echo(" [m] Enter value manually")
165
+ click.echo()
166
+
167
+ while True:
168
+ try:
169
+ choice = click.prompt(
170
+ "Select option",
171
+ default=1,
172
+ type=str,
173
+ show_choices=False,
174
+ ).strip()
175
+ except click.Abort:
176
+ raise
177
+ if choice.lower() == 'm':
178
+ value = _prompt_manual_env_value(key)
179
+ break
180
+
181
+ try:
182
+ index = int(choice)
183
+ except ValueError:
184
+ click.echo('Invalid selection. Enter a number or "m".')
185
+ continue
186
+
187
+ if 1 <= index <= len(options):
188
+ _, value = options[index - 1]
189
+ break
190
+
191
+ click.echo(f"Invalid selection. Enter a number between 1 and {len(options)} or 'm'.")
192
+
193
+ else:
194
+ print(f"No value found for {key}")
195
+ value = _prompt_manual_env_value(key)
196
+
197
+ os.environ[key] = value
198
+ ensure_env_var(key, value)
199
+ print(f"Loaded {key}={mask_str(value)} into process environment")
200
+ return value
201
+
202
+
203
+ def write_env_var_to_dotenv(
204
+ key: str,
205
+ value: str,
206
+ output_file_path: str | Path | None = None,
207
+ ) -> None:
208
+ path = Path(".env") if output_file_path is None else Path(output_file_path)
209
+ path = path.expanduser()
210
+ path.parent.mkdir(parents=True, exist_ok=True)
211
+
212
+ encoded_value = _format_env_value(value)
213
+
214
+ lines: list[str] = []
215
+ key_written = False
216
+
217
+ if path.is_file():
218
+ try:
219
+ with path.open('r', encoding="utf-8") as handle:
220
+ lines = handle.readlines()
221
+ except OSError as exc:
222
+ raise RuntimeError(f"Failed to read {path}: {exc}") from exc
223
+
224
+ for index, line in enumerate(lines):
225
+ parsed = _parse_env_assignment(line)
226
+ if parsed is None or parsed[0] != key:
227
+ continue
228
+
229
+ leading_len = len(line) - len(line.lstrip(' \t'))
230
+ leading = line[:leading_len]
231
+ stripped = line.lstrip()
232
+ has_export = stripped.lower().startswith('export ')
233
+ newline = '\n' if line.endswith('\n') else ''
234
+ prefix = 'export ' if has_export else ''
235
+ lines[index] = f"{leading}{prefix}{key}={encoded_value}{newline}"
236
+ key_written = True
237
+ break
238
+
239
+ if not key_written:
240
+ if lines and not lines[-1].endswith('\n'):
241
+ lines[-1] = f"{lines[-1]}\n"
242
+ lines.append(f"{key}={encoded_value}\n")
243
+
244
+ try:
245
+ with path.open('w', encoding="utf-8") as handle:
246
+ handle.writelines(lines)
247
+ except OSError as exc:
248
+ raise RuntimeError(f"Failed to write {path}: {exc}") from exc
249
+
250
+ print(f"Wrote {key}={mask_str(value)} to {path.resolve()}")
251
+
252
+
253
+ def write_env_var_to_json(
254
+ key: str,
255
+ value: str,
256
+ output_file_path: str | Path,
257
+ ) -> None:
258
+ path = Path(output_file_path).expanduser()
259
+ if path.exists() and not path.is_file():
260
+ raise RuntimeError(f"{path} exists and is not a file")
261
+
262
+ data: dict[str, str] = {}
263
+
264
+ if path.is_file():
265
+ try:
266
+ with path.open('r', encoding="utf-8") as handle:
267
+ existing = json.load(handle)
268
+ except json.JSONDecodeError as exc:
269
+ raise RuntimeError(f"Invalid JSON in {path}: {exc}") from exc
270
+ except OSError as exc:
271
+ raise RuntimeError(f"Failed to read {path}: {exc}") from exc
272
+
273
+ if not isinstance(existing, dict):
274
+ raise RuntimeError(f"Expected JSON object in {path}")
275
+
276
+ for existing_key, existing_value in existing.items():
277
+ if existing_key == key:
278
+ continue
279
+ data[str(existing_key)] = (
280
+ existing_value if isinstance(existing_value, str) else str(existing_value)
281
+ )
282
+
283
+ data[key] = value
284
+
285
+ path.parent.mkdir(parents=True, exist_ok=True)
286
+
287
+ try:
288
+ with path.open('w', encoding="utf-8") as handle:
289
+ json.dump(data, handle, indent=2, sort_keys=True)
290
+ handle.write('\n')
291
+ except OSError as exc:
292
+ raise RuntimeError(f"Failed to write {path}: {exc}") from exc
293
+
294
+ print(f"Wrote {key}={mask_str(value)} to {path}")
synth_ai/utils/http.py ADDED
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import aiohttp
9
+
10
+ __all__ = ["HTTPError", "AsyncHttpClient", "http_request", "sleep"]
11
+
12
+
13
+ @dataclass
14
+ class HTTPError(Exception):
15
+ status: int
16
+ url: str
17
+ message: str
18
+ body_snippet: str | None = None
19
+ detail: Any | None = None
20
+
21
+ def __str__(self) -> str: # pragma: no cover - trivial
22
+ base = f"HTTP {self.status} for {self.url}: {self.message}"
23
+ if self.body_snippet:
24
+ base += f" | body[0:200]={self.body_snippet[:200]}"
25
+ return base
26
+
27
+
28
+ class AsyncHttpClient:
29
+ def __init__(self, base_url: str, api_key: str, timeout: float = 30.0) -> None:
30
+ self._base_url = base_url.rstrip("/")
31
+ self._api_key = api_key
32
+ self._timeout = aiohttp.ClientTimeout(total=timeout)
33
+ self._session: aiohttp.ClientSession | None = None
34
+
35
+ async def __aenter__(self) -> AsyncHttpClient:
36
+ if self._session is None:
37
+ headers = {
38
+ "authorization": f"Bearer {self._api_key}",
39
+ "accept": "application/json",
40
+ }
41
+ user_id = os.getenv("SYNTH_USER_ID") or os.getenv("X_USER_ID") or os.getenv("USER_ID")
42
+ if user_id:
43
+ headers["X-User-ID"] = user_id
44
+ org_id = os.getenv("SYNTH_ORG_ID") or os.getenv("X_ORG_ID") or os.getenv("ORG_ID")
45
+ if org_id:
46
+ headers["X-Org-ID"] = org_id
47
+ self._session = aiohttp.ClientSession(headers=headers, timeout=self._timeout)
48
+ return self
49
+
50
+ async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
51
+ if self._session is not None:
52
+ await self._session.close()
53
+ self._session = None
54
+
55
+ def _abs(self, path: str) -> str:
56
+ if path.startswith("http://") or path.startswith("https://"):
57
+ return path
58
+ if self._base_url.endswith("/api") and path.startswith("/api"):
59
+ path = path[4:]
60
+ return f"{self._base_url}/{path.lstrip('/')}"
61
+
62
+ async def get(
63
+ self,
64
+ path: str,
65
+ *,
66
+ params: dict[str, Any] | None = None,
67
+ headers: dict[str, str] | None = None,
68
+ ) -> Any:
69
+ url = self._abs(path)
70
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
71
+ async with self._session.get(url, params=params, headers=headers) as resp:
72
+ return await self._handle_response(resp, url)
73
+
74
+ async def post_json(
75
+ self, path: str, *, json: dict[str, Any], headers: dict[str, str] | None = None
76
+ ) -> Any:
77
+ url = self._abs(path)
78
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
79
+ async with self._session.post(url, json=json, headers=headers) as resp:
80
+ return await self._handle_response(resp, url)
81
+
82
+ async def post_multipart(
83
+ self,
84
+ path: str,
85
+ *,
86
+ data: dict[str, Any],
87
+ files: dict[str, tuple[str, bytes, str | None]],
88
+ headers: dict[str, str] | None = None,
89
+ ) -> Any:
90
+ url = self._abs(path)
91
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
92
+ form = aiohttp.FormData()
93
+ for k, v in data.items():
94
+ form.add_field(k, str(v))
95
+ for field, (filename, content, content_type) in files.items():
96
+ form.add_field(
97
+ field,
98
+ content,
99
+ filename=filename,
100
+ content_type=content_type or "application/octet-stream",
101
+ )
102
+ async with self._session.post(url, data=form, headers=headers) as resp:
103
+ return await self._handle_response(resp, url)
104
+
105
+ async def delete(self, path: str, *, headers: dict[str, str] | None = None) -> Any:
106
+ url = self._abs(path)
107
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
108
+ async with self._session.delete(url, headers=headers) as resp:
109
+ return await self._handle_response(resp, url)
110
+
111
+ async def _handle_response(self, resp: aiohttp.ClientResponse, url: str) -> Any:
112
+ text = await resp.text()
113
+ body_snippet = text[:200] if text else None
114
+ if 200 <= resp.status < 300:
115
+ ctype = resp.headers.get("content-type", "")
116
+ if "application/json" in ctype:
117
+ try:
118
+ return await resp.json()
119
+ except Exception:
120
+ return text
121
+ return text
122
+ detail: Any | None = None
123
+ try:
124
+ detail = await resp.json()
125
+ except Exception:
126
+ detail = None
127
+ raise HTTPError(
128
+ status=resp.status,
129
+ url=url,
130
+ message="request_failed",
131
+ body_snippet=body_snippet,
132
+ detail=detail,
133
+ )
134
+
135
+
136
+ def http_request(
137
+ method: str, url: str, headers: dict[str, str] | None = None, body: dict[str, Any] | None = None
138
+ ) -> tuple[int, dict[str, Any] | str]:
139
+ import json as _json
140
+ import ssl
141
+ import urllib.error
142
+ import urllib.request
143
+
144
+ data = None
145
+ if body is not None:
146
+ data = _json.dumps(body).encode("utf-8")
147
+ req = urllib.request.Request(url, method=method, headers=headers or {}, data=data)
148
+ try:
149
+ ctx = ssl._create_unverified_context()
150
+ if os.getenv("SYNTH_SSL_VERIFY", "0") == "1":
151
+ ctx = None
152
+ with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
153
+ code = getattr(resp, "status", 200)
154
+ txt = resp.read().decode("utf-8", errors="ignore")
155
+ try:
156
+ return int(code), _json.loads(txt)
157
+ except Exception:
158
+ return int(code), txt
159
+ except urllib.error.HTTPError as exc: # Capture 4xx/5xx bodies
160
+ txt = exc.read().decode("utf-8", errors="ignore")
161
+ try:
162
+ return int(exc.code or 0), _json.loads(txt)
163
+ except Exception:
164
+ return int(exc.code or 0), txt
165
+ except Exception as exc:
166
+ return 0, str(exc)
167
+
168
+
169
+ async def sleep(seconds: float) -> None:
170
+ """Small async sleep helper preserved for backwards compatibility."""
171
+
172
+ await asyncio.sleep(max(float(seconds or 0.0), 0.0))