synth-ai 0.2.16__py3-none-any.whl → 0.2.19__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 (299) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/baseline/banking77_baseline.py +204 -0
  3. examples/baseline/crafter_baseline.py +407 -0
  4. examples/baseline/pokemon_red_baseline.py +326 -0
  5. examples/baseline/simple_baseline.py +56 -0
  6. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  7. examples/blog_posts/gepa/README.md +355 -0
  8. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  9. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
  10. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
  11. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
  12. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
  13. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
  14. examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
  15. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
  16. examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
  17. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
  18. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
  19. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
  20. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
  21. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
  22. examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
  23. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  24. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  25. examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
  26. examples/blog_posts/gepa/task_apps.py +105 -0
  27. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  28. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  29. examples/blog_posts/pokemon_vl/README.md +98 -0
  30. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  31. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  32. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  33. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  34. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  35. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  36. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  37. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  38. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  39. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  40. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  41. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  42. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  43. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  44. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  45. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  46. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  47. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  48. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  49. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  50. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  51. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  52. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  53. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  54. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  55. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  56. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  57. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  58. examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
  59. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
  60. examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
  61. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  62. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
  63. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
  64. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  65. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  66. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  67. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  68. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  69. examples/qwen_vl/README.md +10 -12
  70. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  71. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  72. examples/qwen_vl/collect_data_via_cli.md +76 -84
  73. examples/qwen_vl/collect_vision_traces.py +4 -4
  74. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  75. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  76. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  77. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  78. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  79. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  80. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  81. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  82. examples/qwen_vl/run_vision_comparison.sh +6 -7
  83. examples/rl/README.md +5 -5
  84. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  85. examples/rl/configs/rl_from_base_qwen17.toml +6 -2
  86. examples/rl/task_app/README.md +1 -2
  87. examples/rl/task_app/math_single_step.py +2 -2
  88. examples/run_crafter_demo.sh +2 -2
  89. examples/sft/README.md +1 -1
  90. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  91. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  92. examples/swe/task_app/README.md +32 -2
  93. examples/swe/task_app/grpo_swe_mini.py +4 -0
  94. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  95. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  96. examples/swe/task_app/hosted/inference/openai_client.py +4 -38
  97. examples/swe/task_app/hosted/policy_routes.py +17 -0
  98. examples/swe/task_app/hosted/rollout.py +4 -2
  99. examples/swe/task_app/morph_backend.py +178 -0
  100. examples/task_apps/banking77/__init__.py +6 -0
  101. examples/task_apps/banking77/banking77_task_app.py +841 -0
  102. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  103. examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
  104. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
  105. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
  106. examples/task_apps/crafter/task_app/README.md +1 -1
  107. examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
  108. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  109. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  110. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  111. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
  112. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
  113. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
  114. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
  115. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
  116. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  117. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  118. examples/task_apps/gepa_benchmarks/common.py +260 -0
  119. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  120. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  121. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  122. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  123. examples/task_apps/math/README.md +1 -2
  124. examples/task_apps/pokemon_red/README.md +3 -4
  125. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
  126. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  127. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  128. examples/task_apps/pokemon_red/task_app.py +288 -39
  129. examples/task_apps/sokoban/README.md +2 -3
  130. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  131. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  132. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  133. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  134. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  135. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
  136. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  137. examples/warming_up_to_rl/task_app/README.md +1 -1
  138. examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
  139. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
  147. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  148. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
  149. synth_ai/api/train/builders.py +99 -4
  150. synth_ai/api/train/cli.py +516 -26
  151. synth_ai/api/train/config_finder.py +13 -2
  152. synth_ai/api/train/configs/__init__.py +23 -2
  153. synth_ai/api/train/configs/prompt_learning.py +442 -0
  154. synth_ai/api/train/configs/rl.py +61 -7
  155. synth_ai/api/train/configs/sft.py +6 -2
  156. synth_ai/api/train/configs/shared.py +59 -2
  157. synth_ai/api/train/task_app.py +1 -1
  158. synth_ai/api/train/validators.py +277 -0
  159. synth_ai/auth/credentials.py +119 -0
  160. synth_ai/baseline/__init__.py +25 -0
  161. synth_ai/baseline/config.py +209 -0
  162. synth_ai/baseline/discovery.py +214 -0
  163. synth_ai/baseline/execution.py +146 -0
  164. synth_ai/cli/__init__.py +94 -18
  165. synth_ai/cli/__main__.py +0 -0
  166. synth_ai/cli/claude.py +70 -0
  167. synth_ai/cli/codex.py +84 -0
  168. synth_ai/cli/commands/__init__.py +18 -0
  169. synth_ai/cli/commands/baseline/__init__.py +12 -0
  170. synth_ai/cli/commands/baseline/core.py +637 -0
  171. synth_ai/cli/commands/baseline/list.py +93 -0
  172. synth_ai/cli/commands/demo/__init__.py +6 -0
  173. synth_ai/cli/commands/demo/core.py +163 -0
  174. synth_ai/cli/commands/eval/__init__.py +19 -0
  175. synth_ai/cli/commands/eval/core.py +1112 -0
  176. synth_ai/cli/commands/eval/errors.py +81 -0
  177. synth_ai/cli/commands/eval/validation.py +133 -0
  178. synth_ai/cli/commands/filter/__init__.py +12 -0
  179. synth_ai/cli/commands/filter/core.py +424 -0
  180. synth_ai/cli/commands/filter/errors.py +55 -0
  181. synth_ai/cli/commands/filter/validation.py +77 -0
  182. synth_ai/cli/commands/help/__init__.py +177 -0
  183. synth_ai/cli/commands/help/core.py +72 -0
  184. synth_ai/cli/commands/smoke/__init__.py +7 -0
  185. synth_ai/cli/commands/smoke/core.py +1436 -0
  186. synth_ai/cli/commands/status/__init__.py +64 -0
  187. synth_ai/cli/commands/status/client.py +192 -0
  188. synth_ai/cli/commands/status/config.py +92 -0
  189. synth_ai/cli/commands/status/errors.py +20 -0
  190. synth_ai/cli/commands/status/formatters.py +164 -0
  191. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  192. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  193. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  194. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  195. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  196. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  197. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  198. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  199. synth_ai/cli/commands/status/utils.py +114 -0
  200. synth_ai/cli/commands/train/__init__.py +53 -0
  201. synth_ai/cli/commands/train/core.py +21 -0
  202. synth_ai/cli/commands/train/errors.py +117 -0
  203. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  204. synth_ai/cli/commands/train/judge_validation.py +305 -0
  205. synth_ai/cli/commands/train/validation.py +386 -0
  206. synth_ai/cli/demo.py +30 -158
  207. synth_ai/cli/deploy/__init__.py +43 -0
  208. synth_ai/cli/deploy.py +162 -0
  209. synth_ai/cli/eval/__init__.py +36 -0
  210. synth_ai/cli/eval/core.py +5 -0
  211. synth_ai/cli/eval/errors.py +31 -0
  212. synth_ai/cli/eval/validation.py +5 -0
  213. synth_ai/cli/filter/__init__.py +28 -0
  214. synth_ai/cli/filter/core.py +5 -0
  215. synth_ai/cli/filter/errors.py +23 -0
  216. synth_ai/cli/filter/validation.py +5 -0
  217. synth_ai/cli/legacy_root_backup.py +14 -8
  218. synth_ai/cli/modal_serve/__init__.py +12 -0
  219. synth_ai/cli/modal_serve/core.py +14 -0
  220. synth_ai/cli/modal_serve/errors.py +8 -0
  221. synth_ai/cli/modal_serve/validation.py +11 -0
  222. synth_ai/cli/opencode.py +107 -0
  223. synth_ai/cli/root.py +9 -5
  224. synth_ai/cli/serve/__init__.py +12 -0
  225. synth_ai/cli/serve/core.py +14 -0
  226. synth_ai/cli/serve/errors.py +8 -0
  227. synth_ai/cli/serve/validation.py +11 -0
  228. synth_ai/cli/setup.py +20 -265
  229. synth_ai/cli/status.py +7 -126
  230. synth_ai/cli/task_app_deploy.py +1 -10
  231. synth_ai/cli/task_app_modal_serve.py +4 -9
  232. synth_ai/cli/task_app_serve.py +4 -11
  233. synth_ai/cli/task_apps.py +51 -1480
  234. synth_ai/cli/train/__init__.py +12 -0
  235. synth_ai/cli/train/core.py +21 -0
  236. synth_ai/cli/train/errors.py +8 -0
  237. synth_ai/cli/train/validation.py +24 -0
  238. synth_ai/cli/train.py +1 -14
  239. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  240. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  241. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  242. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  243. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  244. synth_ai/environments/examples/red/engine.py +33 -12
  245. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  246. synth_ai/environments/examples/red/environment.py +26 -0
  247. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  248. synth_ai/http.py +12 -0
  249. synth_ai/judge_schemas.py +10 -10
  250. synth_ai/learning/__init__.py +10 -0
  251. synth_ai/learning/prompt_learning_client.py +276 -0
  252. synth_ai/learning/prompt_learning_types.py +184 -0
  253. synth_ai/learning/rl/client.py +3 -1
  254. synth_ai/pricing/__init__.py +2 -0
  255. synth_ai/pricing/model_pricing.py +57 -0
  256. synth_ai/streaming/__init__.py +29 -0
  257. synth_ai/streaming/config.py +94 -0
  258. synth_ai/streaming/handlers.py +518 -0
  259. synth_ai/streaming/streamer.py +320 -0
  260. synth_ai/streaming/types.py +95 -0
  261. synth_ai/task/apps/__init__.py +1 -0
  262. synth_ai/task/config.py +2 -0
  263. synth_ai/task/tracing_utils.py +25 -25
  264. synth_ai/task/validators.py +45 -9
  265. synth_ai/task_app_cfgs.py +21 -0
  266. synth_ai/tracing_v3/config.py +162 -19
  267. synth_ai/tracing_v3/constants.py +1 -1
  268. synth_ai/tracing_v3/db_config.py +24 -38
  269. synth_ai/tracing_v3/migration_helper.py +1 -2
  270. synth_ai/tracing_v3/storage/config.py +47 -13
  271. synth_ai/tracing_v3/storage/factory.py +3 -3
  272. synth_ai/tracing_v3/turso/daemon.py +113 -11
  273. synth_ai/tracing_v3/turso/native_manager.py +92 -16
  274. synth_ai/types.py +8 -0
  275. synth_ai/urls.py +11 -0
  276. synth_ai/utils/__init__.py +30 -1
  277. synth_ai/utils/agents.py +74 -0
  278. synth_ai/utils/bin.py +39 -0
  279. synth_ai/utils/cli.py +149 -5
  280. synth_ai/utils/env.py +40 -33
  281. synth_ai/utils/http.py +4 -1
  282. synth_ai/utils/json.py +72 -0
  283. synth_ai/utils/modal.py +285 -3
  284. synth_ai/utils/paths.py +48 -0
  285. synth_ai/utils/uvicorn.py +113 -0
  286. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
  287. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
  288. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  289. synth_ai/cli/tui.py +0 -62
  290. synth_ai/tui/__init__.py +0 -5
  291. synth_ai/tui/__main__.py +0 -13
  292. synth_ai/tui/cli/__init__.py +0 -1
  293. synth_ai/tui/cli/query_experiments.py +0 -164
  294. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  295. synth_ai/tui/dashboard.py +0 -911
  296. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
  297. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
  298. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
  299. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
@@ -14,8 +14,11 @@ import re
14
14
  from collections.abc import Callable
15
15
  from dataclasses import asdict, dataclass
16
16
  from datetime import UTC, datetime
17
+ from pathlib import Path
17
18
  from typing import TYPE_CHECKING, Any, cast
19
+ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
18
20
 
21
+ import httpx
19
22
  import libsql
20
23
  from sqlalchemy.engine import make_url
21
24
 
@@ -60,36 +63,70 @@ class _ConnectionTarget:
60
63
  auth_token: str | None = None
61
64
 
62
65
 
66
+ def _strip_auth_component(url: str) -> tuple[str, str | None]:
67
+ """Remove auth_token query parameter from URL, returning the token separately."""
68
+ parsed = urlparse(url)
69
+ if not parsed.query:
70
+ return url, None
71
+
72
+ params = dict(parse_qsl(parsed.query, keep_blank_values=True))
73
+ token = params.pop("auth_token", None)
74
+ query = urlencode(params, doseq=True)
75
+ sanitised = urlunparse(parsed._replace(query=query))
76
+ return sanitised, token
77
+
78
+
63
79
  def _resolve_connection_target(db_url: str | None, auth_token: str | None) -> _ConnectionTarget:
64
80
  """Normalise the configured database URL."""
65
81
  url = db_url or CONFIG.db_url
66
-
67
- # Fast-path local SQLite URLs (`sqlite+aiosqlite:///path/to/db`)
68
- if url.startswith("sqlite+aiosqlite:///"):
69
- return _ConnectionTarget(database=url.replace("sqlite+aiosqlite:///", ""), auth_token=auth_token)
82
+ sanitised, token_from_url = _strip_auth_component(url)
83
+ effective_token = auth_token or token_from_url or CONFIG.auth_token
70
84
 
71
85
  # SQLAlchemy-compatible libsql scheme (`sqlite+libsql://<endpoint or path>`)
72
- if url.startswith("sqlite+libsql://"):
73
- target = url.replace("sqlite+libsql://", "", 1)
74
- return _ConnectionTarget(database=target, sync_url=target if target.startswith("libsql://") else None, auth_token=auth_token)
86
+ if sanitised.startswith("sqlite+libsql://"):
87
+ raise RuntimeError("sqlite+libsql scheme is no longer supported; use libsql://")
88
+
89
+ # Plain SQLite files: file://, /absolute/path, or relative path
90
+ # libsql.connect() handles these without sync_url or auth_token
91
+ if sanitised.startswith("file://") or sanitised.startswith("/") or "://" not in sanitised:
92
+ # Strip file:// prefix if present, libsql.connect handles both formats
93
+ db_path = sanitised.replace("file://", "") if sanitised.startswith("file://") else sanitised
94
+ return _ConnectionTarget(database=db_path, sync_url=None, auth_token=None)
75
95
 
76
96
  # Native libsql URLs (`libsql://...`).
77
- if url.startswith("libsql://"):
78
- return _ConnectionTarget(database=url, sync_url=url, auth_token=auth_token)
97
+ if sanitised.startswith("libsql://"):
98
+ return _ConnectionTarget(database=sanitised, sync_url=sanitised, auth_token=effective_token)
79
99
 
80
100
  # Fallback to SQLAlchemy URL parsing for anything else we missed.
81
101
  try:
82
- parsed = make_url(url)
83
- if parsed.drivername.startswith("sqlite") and parsed.database:
84
- return _ConnectionTarget(database=parsed.database, auth_token=auth_token)
85
- if parsed.drivername.startswith("libsql"):
102
+ parsed = make_url(sanitised)
103
+ driver = parsed.drivername.lower()
104
+ if driver.startswith("sqlite"):
105
+ database = parsed.database or ""
106
+ if database and database not in {":memory:", ":memory"}:
107
+ # Absolute paths are passed through; relative paths are resolved to cwd
108
+ if database.startswith("/"):
109
+ db_path = database
110
+ else:
111
+ db_path = str(Path(database).expanduser().resolve())
112
+ elif database in {":memory:", ":memory"}:
113
+ db_path = ":memory:"
114
+ else:
115
+ raise RuntimeError("SQLite URL missing database path.")
116
+ return _ConnectionTarget(database=db_path, sync_url=None, auth_token=None)
117
+ if driver.startswith("libsql"):
86
118
  database = parsed.render_as_string(hide_password=False)
87
- return _ConnectionTarget(database=database, sync_url=database, auth_token=auth_token)
119
+ return _ConnectionTarget(database=database, sync_url=database, auth_token=effective_token)
88
120
  except Exception: # pragma: no cover - defensive guardrail
89
121
  logger.debug("Unable to parse db_url via SQLAlchemy", exc_info=True)
90
122
 
91
- # As a last resort use the raw value (libsql.connect can handle absolute paths).
92
- return _ConnectionTarget(database=url, auth_token=auth_token)
123
+ # Python libsql client uses HTTP API for http:// URLs, not Hrana WebSocket
124
+ # For local sqld with http:// URL, we need to ensure it points to the HTTP API port
125
+ # sqld uses two ports: Hrana WebSocket (e.g. 8080) and HTTP API (e.g. 8081)
126
+ # libsql.connect() with http:// uses HTTP API, so URL should point to HTTP API port
127
+ if sanitised.startswith(("http://", "https://", "libsql://")):
128
+ return _ConnectionTarget(database=sanitised, sync_url=sanitised, auth_token=effective_token)
129
+ raise RuntimeError(f"Unsupported tracing database URL: {sanitised}")
93
130
 
94
131
 
95
132
  def _json_dumps(value: Any) -> str | None:
@@ -350,6 +387,45 @@ class NativeLibsqlTraceManager(TraceStorage):
350
387
  if self._initialized:
351
388
  return
352
389
 
390
+ # Fast-fail preflight: if using remote endpoint or local sqld, check health
391
+ # Skip health check for plain SQLite files (sync_url is None)
392
+ if self._target.sync_url:
393
+ try:
394
+ parsed = urlparse(self._target.database or "")
395
+ # Check for local sqld: http://, https://, or libsql://
396
+ if parsed.scheme in ("http", "https", "libsql"):
397
+ host_port = parsed.netloc or ""
398
+ host = (host_port.split(":", 1)[0] or "").strip().lower()
399
+ if host in {"127.0.0.1", "localhost"} and host_port:
400
+ # For http:// URLs, the port should already be the HTTP API port
401
+ # For libsql:// URLs, we need to calculate health check port
402
+ if ":" in host_port:
403
+ port = int(host_port.split(":", 1)[1])
404
+ if parsed.scheme == "libsql":
405
+ # libsql:// uses Hrana port, health check is on HTTP API port (Hrana + 1)
406
+ health_url = f"http://{host}:{port + 1}/health"
407
+ else:
408
+ # http:// already points to HTTP API port
409
+ health_url = f"http://{host}:{port}/health"
410
+ else:
411
+ health_url = f"http://{host_port}/health"
412
+ try:
413
+ async with httpx.AsyncClient(timeout=httpx.Timeout(1.0)) as client:
414
+ resp = await client.get(health_url)
415
+ if resp.status_code != 200:
416
+ raise RuntimeError(
417
+ f"Tracing backend unhealthy at {health_url} (status={resp.status_code})"
418
+ )
419
+ except Exception as exc: # pragma: no cover - network env dependent
420
+ raise RuntimeError(
421
+ f"Tracing backend not reachable at {health_url}. "
422
+ f"Start sqld with both ports: sqld --db-path <path> --hrana-listen-addr {host}:HRANA_PORT --http-listen-addr {host}:HTTP_PORT "
423
+ f"or disable tracing (TASKAPP_TRACING_ENABLED=0)."
424
+ ) from exc
425
+ except Exception:
426
+ # Propagate any preflight failure to abort early
427
+ raise
428
+
353
429
  # Establish a libsql connection for future native operations.
354
430
  self._conn = self._open_connection()
355
431
  self._ensure_schema()
synth_ai/types.py ADDED
@@ -0,0 +1,8 @@
1
+ import typing
2
+ from typing import Literal
3
+
4
+ ModelName = Literal[
5
+ "synth-small",
6
+ "synth-medium"
7
+ ]
8
+ MODEL_NAMES = list(typing.get_args(ModelName))
synth_ai/urls.py ADDED
@@ -0,0 +1,11 @@
1
+ # Base URL for all backends
2
+ BACKEND_URL_BASE = "https://agent-learning.onrender.com"
3
+
4
+ # Synth Research API base (supports OpenAI, Anthropic, and custom formats)
5
+ # Real routes: /api/synth-research/chat/completions, /api/synth-research/messages
6
+ # V1 routes: /api/synth-research/v1/chat/completions, /api/synth-research/v1/messages
7
+ BACKEND_URL_SYNTH_RESEARCH_BASE = BACKEND_URL_BASE + "/api/synth-research"
8
+
9
+ # Provider-specific URLs (for SDKs that expect standard paths)
10
+ BACKEND_URL_SYNTH_RESEARCH_OPENAI = BACKEND_URL_SYNTH_RESEARCH_BASE + "/v1" # For OpenAI SDKs (appends /chat/completions)
11
+ BACKEND_URL_SYNTH_RESEARCH_ANTHROPIC = BACKEND_URL_SYNTH_RESEARCH_BASE # For Anthropic SDKs (appends /v1/messages)
@@ -1,8 +1,18 @@
1
1
  from . import task_app_state
2
+ from .agents import write_agents_md
2
3
  from .base_url import PROD_BASE_URL_DEFAULT, get_backend_from_env, get_learning_v2_base_url
3
- from .cli import PromptedChoiceOption, PromptedChoiceType, print_next_step
4
+ from .bin import install_bin, verify_bin
5
+ from .cli import (
6
+ PromptedChoiceOption,
7
+ PromptedChoiceType,
8
+ PromptedPathOption,
9
+ print_next_step,
10
+ prompt_choice,
11
+ prompt_for_path,
12
+ )
4
13
  from .env import mask_str, resolve_env_var, write_env_var_to_dotenv, write_env_var_to_json
5
14
  from .http import AsyncHttpClient, HTTPError, http_request
15
+ from .json import create_and_write_json, load_json_to_dict, strip_json_comments
6
16
  from .modal import (
7
17
  ensure_modal_installed,
8
18
  ensure_task_app_ready,
@@ -11,6 +21,12 @@ from .modal import (
11
21
  is_modal_public_url,
12
22
  normalize_endpoint_url,
13
23
  )
24
+ from .paths import (
25
+ find_bin_path,
26
+ find_config_path,
27
+ get_env_file_paths,
28
+ get_home_config_file_paths,
29
+ )
14
30
  from .process import ensure_local_port_available, popen_capture, popen_stream, popen_stream_capture
15
31
  from .sqld import SQLD_VERSION, find_sqld_binary, install_sqld
16
32
  from .task_app_discovery import AppChoice, discover_eval_config_paths, select_app_choice
@@ -50,8 +66,11 @@ __all__ = [
50
66
  "PROD_BASE_URL_DEFAULT",
51
67
  "PromptedChoiceOption",
52
68
  "PromptedChoiceType",
69
+ "PromptedPathOption",
70
+ "prompt_for_path",
53
71
  "SQLD_VERSION",
54
72
  "USER_CONFIG_PATH",
73
+ "create_and_write_json",
55
74
  "current_task_app_id",
56
75
  "discover_eval_config_paths",
57
76
  "ensure_env_credentials",
@@ -60,14 +79,20 @@ __all__ = [
60
79
  "ensure_port_free",
61
80
  "ensure_task_app_ready",
62
81
  "find_asgi_apps",
82
+ "find_bin_path",
83
+ "find_config_path",
63
84
  "find_sqld_binary",
64
85
  "get_backend_from_env",
86
+ "get_env_file_paths",
87
+ "get_home_config_file_paths",
65
88
  "get_learning_v2_base_url",
66
89
  "http_request",
90
+ "install_bin",
67
91
  "install_sqld",
68
92
  "is_local_demo_url",
69
93
  "is_modal_public_url",
70
94
  "load_demo_dir",
95
+ "load_json_to_dict",
71
96
  "load_template_id",
72
97
  "load_user_config",
73
98
  "load_user_env",
@@ -84,17 +109,21 @@ __all__ = [
84
109
  "popen_stream_capture",
85
110
  "preflight_env_key",
86
111
  "print_next_step",
112
+ "prompt_choice",
87
113
  "read_task_app_config",
88
114
  "record_task_app",
89
115
  "resolve_env_var",
90
116
  "resolve_task_app_entry",
91
117
  "save_user_config",
92
118
  "select_app_choice",
119
+ "strip_json_comments",
93
120
  "task_app_config_path",
94
121
  "task_app_id_from_path",
95
122
  "task_app_state",
96
123
  "update_task_app_entry",
97
124
  "update_user_config",
125
+ "verify_bin",
126
+ 'write_agents_md',
98
127
  "write_env_var_to_dotenv",
99
128
  "write_env_var_to_json",
100
129
  "write_task_app_config",
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+
3
+ AGENTS_TEXT = """
4
+ sinf
5
+ """
6
+ SYNTH_DIV_START = "### --- SYNTH SECTION START ---"
7
+ SYNTH_DIV_END = "### ---- SYNTH SECTION END ----"
8
+
9
+
10
+ def _render_block() -> str:
11
+ return f"{SYNTH_DIV_START}\n{AGENTS_TEXT}\n{SYNTH_DIV_END}"
12
+
13
+
14
+ def _append_block(prefix: str) -> str:
15
+ prefix = prefix.rstrip()
16
+ block = _render_block()
17
+ if prefix:
18
+ return f"{prefix}\n\n{block}\n"
19
+ return f"{block}\n"
20
+
21
+
22
+ def write_agents_md() -> None:
23
+ path = Path.cwd() / "AGENTS.md"
24
+ if not path.exists():
25
+ path.write_text(_append_block(""), encoding="utf-8")
26
+ return
27
+
28
+ file_text = path.read_text(encoding="utf-8")
29
+
30
+ # Remove orphan end markers first (end markers without a preceding start marker)
31
+ cleaned = file_text
32
+ end_pos = cleaned.find(SYNTH_DIV_END)
33
+ start_pos = cleaned.find(SYNTH_DIV_START)
34
+
35
+ # If there's an end marker before any start marker
36
+ if end_pos != -1 and (start_pos == -1 or end_pos < start_pos):
37
+ if start_pos == -1:
38
+ # No start markers at all - remove everything including content before orphan
39
+ cleaned = cleaned[end_pos + len(SYNTH_DIV_END):].lstrip()
40
+ else:
41
+ # There are start markers after the orphan - preserve content before orphan
42
+ before_orphan = cleaned[:end_pos].rstrip()
43
+ after_orphan = cleaned[end_pos + len(SYNTH_DIV_END):].lstrip()
44
+ cleaned = "\n\n".join(filter(None, [before_orphan, after_orphan]))
45
+
46
+ # Find the first start and last end marker to consolidate multiple sections
47
+ first_start = cleaned.find(SYNTH_DIV_START)
48
+ last_end = cleaned.rfind(SYNTH_DIV_END)
49
+
50
+ if first_start != -1 and last_end != -1 and last_end > first_start:
51
+ # We have at least one valid section, consolidate all into one
52
+ before = cleaned[:first_start].rstrip()
53
+ after = cleaned[last_end + len(SYNTH_DIV_END):].lstrip()
54
+
55
+ parts: list[str] = []
56
+ if before:
57
+ parts.append(before)
58
+ parts.append(_render_block())
59
+ if after:
60
+ parts.append(after)
61
+
62
+ new_text = "\n\n".join(parts)
63
+ if not new_text.endswith("\n"):
64
+ new_text += "\n"
65
+ path.write_text(new_text, encoding="utf-8")
66
+ return
67
+
68
+ # No valid sections found, remove any remaining orphan markers
69
+ cleaned = cleaned.replace(SYNTH_DIV_END, "")
70
+ cleaned = cleaned.replace(AGENTS_TEXT, "")
71
+ cleaned = cleaned.strip()
72
+ if cleaned:
73
+ cleaned += "\n\n"
74
+ path.write_text(f"{cleaned}{_render_block()}\n", encoding="utf-8")
synth_ai/utils/bin.py ADDED
@@ -0,0 +1,39 @@
1
+ import shlex
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ from .cli import prompt_choice
6
+
7
+
8
+ def install_bin(name: str, install_options: list[str]) -> bool:
9
+ cmd = prompt_choice(
10
+ f"How would you like to install {name}?",
11
+ install_options
12
+ )
13
+ div_start = f"{'-' * 29} INSTALL START {'-' * 29}"
14
+ div_end = f"{'-' * 30} INSTALL END {'-' * 30}"
15
+ try:
16
+ print(f"Installing {name} via `{cmd}`")
17
+ print('\n' + div_start)
18
+ subprocess.run(shlex.split(cmd), check=True)
19
+ print(div_end + '\n')
20
+ return True
21
+ except subprocess.CalledProcessError as e:
22
+ print(f"Failed to install {name}: {e}")
23
+ print(div_end + '\n')
24
+ return False
25
+
26
+
27
+ def verify_bin(bin_path: Path) -> bool:
28
+ try:
29
+ result = subprocess.run(
30
+ [str(bin_path), "--version"],
31
+ capture_output=True,
32
+ text=True,
33
+ timeout=3,
34
+ check=False
35
+ )
36
+ return result.returncode == 0
37
+ except (OSError, subprocess.SubprocessError) as e:
38
+ print(e)
39
+ return False
synth_ai/utils/cli.py CHANGED
@@ -1,9 +1,29 @@
1
1
  from collections.abc import Sequence
2
- from typing import Any, cast
2
+ from pathlib import Path
3
+ from typing import Any, Callable, cast
3
4
 
4
5
  import click
5
6
 
6
7
 
8
+ def prompt_choice(msg: str, choices: list[str]) -> str:
9
+ print(msg)
10
+ for i, label in enumerate(choices, start=1):
11
+ print(f" [{i}] {label}")
12
+ while True:
13
+ try:
14
+ choice = click.prompt(
15
+ "Select an option",
16
+ default=1,
17
+ type=int,
18
+ show_choices=False
19
+ )
20
+ except click.Abort:
21
+ raise
22
+ if 1 <= choice <= len(choices):
23
+ return choices[choice - 1]
24
+ print(f"Invalid selection. Enter a number between 1 and {len(choices)}")
25
+
26
+
7
27
  class PromptedChoiceType(click.Choice):
8
28
  """`click.Choice` variant that reprompts with an interactive menu on failure.
9
29
 
@@ -61,9 +81,15 @@ class PromptedChoiceType(click.Choice):
61
81
  for index, choice in enumerate(self.choices, 1):
62
82
  click.echo(f" [{index}] {choice}")
63
83
  while True:
64
- selection = click.prompt("> ", type=int)
65
- if 1 <= selection <= len(self.choices):
66
- return cast(str, self.choices[selection - 1])
84
+ choice = click.prompt(
85
+ "Select an option",
86
+ default=1,
87
+ type=int,
88
+ show_choices=False
89
+ )
90
+ if 1 <= choice <= len(self.choices):
91
+ print('')
92
+ return cast(str, self.choices[choice - 1])
67
93
  click.echo(f"Invalid selection for {arg_name}, please try again")
68
94
 
69
95
  def _get_cmd_name(self, ctx: click.Context | None) -> str:
@@ -122,7 +148,125 @@ class PromptedChoiceOption(click.Option):
122
148
  if isinstance(option_type, PromptedChoiceType):
123
149
  return option_type._prompt_user(self, ctx)
124
150
  return super().prompt_for_value(ctx)
125
-
151
+
152
+
153
+ def prompt_for_path(
154
+ label: str,
155
+ *,
156
+ available_paths: Sequence[str | Path] | None = None,
157
+ file_type: str | None = None,
158
+ path_type: click.Path | None = None,
159
+ ) -> Path:
160
+ """Prompt for a filesystem path, optionally offering curated choices."""
161
+
162
+ def _normalize_suffix(ext: str | None) -> str | None:
163
+ if not ext:
164
+ return None
165
+ stripped = ext.strip()
166
+ if not stripped:
167
+ return None
168
+ if not stripped.startswith("."):
169
+ stripped = f".{stripped}"
170
+ return stripped.lower()
171
+
172
+ def _format_label(text: str) -> str:
173
+ return text.strip() or "path"
174
+
175
+ expected_suffix = _normalize_suffix(file_type)
176
+ prompt_label = _format_label(label)
177
+
178
+ path_type = path_type or click.Path(
179
+ exists=True,
180
+ dir_okay=False,
181
+ file_okay=True,
182
+ path_type=Path,
183
+ )
184
+
185
+ candidates: list[str] = []
186
+ if available_paths:
187
+ seen: set[str] = set()
188
+ for entry in available_paths:
189
+ candidate = str(Path(entry))
190
+ suffix = Path(candidate).suffix.lower()
191
+ if candidate in seen:
192
+ continue
193
+ if expected_suffix and suffix != expected_suffix:
194
+ continue
195
+ seen.add(candidate)
196
+ candidates.append(candidate)
197
+
198
+ ctx = click.get_current_context(silent=True)
199
+
200
+ while True:
201
+ if candidates:
202
+ click.echo(f"\nPlease choose a {prompt_label}:")
203
+ for index, option in enumerate(candidates, 1):
204
+ click.echo(f" [{index}] {option}")
205
+ custom_index = len(candidates) + 1
206
+ click.echo(f" [{custom_index}] Enter a custom path")
207
+
208
+ selection = click.prompt("> ", type=int)
209
+ if 1 <= selection <= len(candidates):
210
+ raw_value = candidates[selection - 1]
211
+ elif selection == custom_index:
212
+ raw_value = click.prompt(prompt_label, type=path_type)
213
+ else:
214
+ click.echo("Invalid selection, please try again")
215
+ continue
216
+ else:
217
+ raw_value = click.prompt(prompt_label, type=path_type)
218
+
219
+ try:
220
+ converted = path_type.convert(str(raw_value), None, ctx)
221
+ except click.BadParameter as exc:
222
+ click.echo(str(exc))
223
+ continue
224
+
225
+ result = converted if isinstance(converted, Path) else Path(converted)
226
+ if expected_suffix and result.suffix.lower() != expected_suffix:
227
+ click.echo(f"Expected a {expected_suffix} file. Received: {result}")
228
+ continue
229
+
230
+ return result
231
+
232
+
233
+ class PromptedPathOption(click.Option):
234
+ """Option that prompts for a filesystem path when omitted."""
235
+
236
+ def __init__(
237
+ self,
238
+ *args: Any,
239
+ available_paths: Sequence[str | Path] | None = None,
240
+ file_type: str | None = None,
241
+ path_type: click.Path | None = None,
242
+ prompt_guard: Callable[[click.Context], bool] | None = None,
243
+ **kwargs: Any,
244
+ ) -> None:
245
+ self._available_paths = available_paths
246
+ self._file_type = file_type
247
+ self._path_type = path_type
248
+ self._prompt_guard = prompt_guard
249
+ kwargs.setdefault("prompt", True)
250
+ kwargs.setdefault("prompt_required", True)
251
+ super().__init__(*args, **kwargs)
252
+
253
+ def prompt_for_value(self, ctx: click.Context) -> Any:
254
+ if not ctx:
255
+ return super().prompt_for_value(ctx)
256
+ if self._prompt_guard is not None:
257
+ try:
258
+ if not self._prompt_guard(ctx):
259
+ return None
260
+ except Exception:
261
+ return None
262
+ label = self.help or self.name or "path"
263
+ return prompt_for_path(
264
+ label,
265
+ available_paths=self._available_paths,
266
+ file_type=self._file_type,
267
+ path_type=self._path_type or getattr(self, "type", None),
268
+ )
269
+
126
270
 
127
271
  def print_next_step(message: str, lines: Sequence[str]) -> None:
128
272
  print(f"\n➡️ Next, {message}:")