synth-ai 0.2.13.dev2__py3-none-any.whl → 0.2.16__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 (293) hide show
  1. examples/README.md +1 -0
  2. examples/multi_step/SFT_README.md +147 -0
  3. examples/multi_step/configs/README_verilog_rl.md +77 -0
  4. examples/multi_step/configs/VERILOG_REWARDS.md +90 -0
  5. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +183 -0
  6. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  7. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  8. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +12 -11
  9. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  10. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  11. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  12. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  13. examples/multi_step/configs/verilog_rl_lora.toml +190 -0
  14. examples/multi_step/convert_traces_to_sft.py +84 -0
  15. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  16. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  17. examples/multi_step/readme.md +48 -0
  18. examples/multi_step/run_sft_qwen30b.sh +45 -0
  19. examples/multi_step/verilog_rl_lora.md +218 -0
  20. examples/qwen_coder/configs/coder_lora_30b.toml +3 -2
  21. examples/qwen_coder/configs/coder_lora_4b.toml +2 -1
  22. examples/qwen_coder/configs/coder_lora_small.toml +2 -1
  23. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  24. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  25. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  26. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  27. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  28. examples/qwen_vl/QUICKSTART.md +327 -0
  29. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  30. examples/qwen_vl/README.md +154 -0
  31. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  32. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  33. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  34. examples/qwen_vl/SETUP_COMPLETE.md +275 -0
  35. examples/qwen_vl/VISION_TESTS_COMPLETE.md +490 -0
  36. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  37. examples/qwen_vl/__init__.py +2 -0
  38. examples/qwen_vl/collect_data_via_cli.md +423 -0
  39. examples/qwen_vl/collect_vision_traces.py +368 -0
  40. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +127 -0
  41. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +60 -0
  42. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +43 -0
  43. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  44. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +45 -0
  45. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +44 -0
  46. examples/qwen_vl/configs/filter_qwen2vl_sft.toml +50 -0
  47. examples/qwen_vl/configs/filter_vision_sft.toml +53 -0
  48. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  49. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  50. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  51. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  52. examples/qwen_vl/run_vision_comparison.sh +62 -0
  53. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  54. examples/qwen_vl/test_image_validation.py +201 -0
  55. examples/qwen_vl/test_sft_vision_data.py +110 -0
  56. examples/rl/README.md +1 -1
  57. examples/rl/configs/eval_base_qwen.toml +17 -0
  58. examples/rl/configs/eval_rl_qwen.toml +13 -0
  59. examples/rl/configs/rl_from_base_qwen.toml +37 -0
  60. examples/rl/configs/rl_from_base_qwen17.toml +76 -0
  61. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  62. examples/rl/run_eval.py +436 -0
  63. examples/rl/run_rl_and_save.py +111 -0
  64. examples/rl/task_app/README.md +22 -0
  65. examples/rl/task_app/math_single_step.py +990 -0
  66. examples/rl/task_app/math_task_app.py +111 -0
  67. examples/sft/README.md +5 -5
  68. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -2
  69. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -3
  70. examples/sft/evaluate.py +4 -4
  71. examples/sft/export_dataset.py +7 -4
  72. examples/sft/generate_traces.py +2 -0
  73. examples/swe/task_app/README.md +1 -1
  74. examples/swe/task_app/grpo_swe_mini.py +1 -1
  75. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  76. examples/swe/task_app/hosted/envs/mini_swe/environment.py +13 -13
  77. examples/swe/task_app/hosted/policy_routes.py +0 -2
  78. examples/swe/task_app/hosted/rollout.py +2 -8
  79. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  80. examples/task_apps/crafter/CREATE_SFT_DATASET.md +273 -0
  81. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  82. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +174 -0
  83. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +268 -0
  84. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  85. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  86. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  87. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  88. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  89. examples/task_apps/crafter/task_app/__init__.py +3 -0
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +309 -14
  91. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +10 -0
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +75 -4
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +17 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +55 -3
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +114 -32
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +127 -27
  97. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +156 -0
  98. examples/task_apps/enron/__init__.py +1 -0
  99. examples/task_apps/enron/filter_sft.toml +5 -0
  100. examples/task_apps/enron/tests/__init__.py +2 -0
  101. examples/task_apps/enron/tests/integration/__init__.py +2 -0
  102. examples/task_apps/enron/tests/integration/test_enron_eval.py +2 -0
  103. examples/task_apps/enron/tests/unit/__init__.py +2 -0
  104. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  105. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  106. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +415 -0
  107. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +29 -0
  108. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +2 -0
  109. examples/task_apps/pokemon_red/task_app.py +199 -6
  110. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +2 -0
  111. examples/task_apps/sokoban/filter_sft.toml +5 -0
  112. examples/task_apps/sokoban/tests/__init__.py +2 -0
  113. examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
  114. examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
  115. examples/task_apps/verilog/eval_groq_qwen32b.toml +8 -4
  116. examples/task_apps/verilog/filter_sft.toml +5 -0
  117. examples/task_apps/verilog/task_app/grpo_verilog.py +258 -23
  118. examples/task_apps/verilog/tests/__init__.py +2 -0
  119. examples/task_apps/verilog/tests/integration/__init__.py +2 -0
  120. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +2 -0
  121. examples/task_apps/verilog/tests/unit/__init__.py +2 -0
  122. examples/vlm/README.md +3 -3
  123. examples/vlm/configs/crafter_vlm_gpt4o.toml +2 -0
  124. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  125. examples/vlm/filter_image_rows.py +1 -1
  126. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  127. examples/warming_up_to_rl/_utils.py +92 -0
  128. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  129. examples/warming_up_to_rl/configs/crafter_fft.toml +2 -0
  130. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +2 -0
  131. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  132. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  133. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  134. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  135. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  136. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  137. examples/warming_up_to_rl/groq_test.py +2 -0
  138. examples/warming_up_to_rl/readme.md +63 -132
  139. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  140. examples/warming_up_to_rl/run_local_rollout.py +2 -0
  141. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -0
  142. examples/warming_up_to_rl/run_local_rollout_parallel.py +2 -0
  143. examples/warming_up_to_rl/run_local_rollout_traced.py +2 -0
  144. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  145. examples/warming_up_to_rl/run_rollout_remote.py +2 -0
  146. examples/warming_up_to_rl/task_app/README.md +42 -0
  147. examples/warming_up_to_rl/task_app/grpo_crafter.py +696 -0
  148. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  152. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  153. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  154. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  155. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  156. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  157. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +478 -0
  158. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  159. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  160. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  161. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  162. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  163. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  164. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  165. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1081 -0
  166. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  167. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  168. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  169. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  170. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  171. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  172. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  173. synth_ai/__init__.py +44 -30
  174. synth_ai/_utils/__init__.py +47 -0
  175. synth_ai/_utils/base_url.py +10 -0
  176. synth_ai/_utils/http.py +10 -0
  177. synth_ai/_utils/prompts.py +10 -0
  178. synth_ai/_utils/task_app_state.py +12 -0
  179. synth_ai/_utils/user_config.py +10 -0
  180. synth_ai/api/models/supported.py +145 -7
  181. synth_ai/api/train/__init__.py +13 -1
  182. synth_ai/api/train/cli.py +30 -7
  183. synth_ai/api/train/config_finder.py +18 -11
  184. synth_ai/api/train/env_resolver.py +13 -10
  185. synth_ai/cli/__init__.py +66 -49
  186. synth_ai/cli/_modal_wrapper.py +9 -6
  187. synth_ai/cli/_typer_patch.py +0 -2
  188. synth_ai/cli/_validate_task_app.py +22 -4
  189. synth_ai/cli/legacy_root_backup.py +3 -1
  190. synth_ai/cli/lib/__init__.py +10 -0
  191. synth_ai/cli/lib/task_app_discovery.py +7 -0
  192. synth_ai/cli/lib/task_app_env.py +518 -0
  193. synth_ai/cli/recent.py +1 -0
  194. synth_ai/cli/setup.py +266 -0
  195. synth_ai/cli/task_app_deploy.py +16 -0
  196. synth_ai/cli/task_app_list.py +25 -0
  197. synth_ai/cli/task_app_modal_serve.py +16 -0
  198. synth_ai/cli/task_app_serve.py +18 -0
  199. synth_ai/cli/task_apps.py +392 -141
  200. synth_ai/cli/train.py +18 -0
  201. synth_ai/cli/tui.py +62 -0
  202. synth_ai/demos/__init__.py +10 -0
  203. synth_ai/demos/core/__init__.py +28 -1
  204. synth_ai/demos/crafter/__init__.py +1 -0
  205. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  206. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  207. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  208. synth_ai/demos/demo_registry.py +176 -0
  209. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  210. synth_ai/demos/math/__init__.py +1 -0
  211. synth_ai/demos/math/_common.py +16 -0
  212. synth_ai/demos/math/app.py +38 -0
  213. synth_ai/demos/math/config.toml +76 -0
  214. synth_ai/demos/math/deploy_modal.py +54 -0
  215. synth_ai/demos/math/modal_task_app.py +702 -0
  216. synth_ai/demos/math/task_app_entry.py +51 -0
  217. synth_ai/environments/environment/core.py +7 -1
  218. synth_ai/environments/examples/bandit/engine.py +0 -1
  219. synth_ai/environments/examples/bandit/environment.py +0 -1
  220. synth_ai/environments/examples/crafter_classic/environment.py +1 -1
  221. synth_ai/environments/examples/verilog/engine.py +76 -10
  222. synth_ai/environments/examples/wordle/environment.py +0 -1
  223. synth_ai/evals/base.py +16 -5
  224. synth_ai/evals/client.py +1 -1
  225. synth_ai/inference/client.py +1 -1
  226. synth_ai/learning/client.py +1 -1
  227. synth_ai/learning/health.py +1 -1
  228. synth_ai/learning/jobs.py +1 -1
  229. synth_ai/learning/rl/client.py +1 -1
  230. synth_ai/learning/rl/env_keys.py +1 -1
  231. synth_ai/learning/rl/secrets.py +1 -1
  232. synth_ai/learning/sft/client.py +1 -1
  233. synth_ai/learning/sft/data.py +407 -4
  234. synth_ai/learning/validators.py +4 -1
  235. synth_ai/task/__init__.py +11 -1
  236. synth_ai/task/apps/__init__.py +5 -2
  237. synth_ai/task/config.py +259 -0
  238. synth_ai/task/contracts.py +15 -2
  239. synth_ai/task/rubrics/__init__.py +4 -2
  240. synth_ai/task/rubrics/loaders.py +27 -4
  241. synth_ai/task/rubrics/scoring.py +3 -0
  242. synth_ai/task/rubrics.py +219 -0
  243. synth_ai/task/trace_correlation_helpers.py +328 -0
  244. synth_ai/task/tracing_utils.py +14 -3
  245. synth_ai/task/validators.py +145 -2
  246. synth_ai/tracing_v3/config.py +15 -13
  247. synth_ai/tracing_v3/constants.py +21 -0
  248. synth_ai/tracing_v3/db_config.py +3 -1
  249. synth_ai/tracing_v3/decorators.py +10 -7
  250. synth_ai/tracing_v3/session_tracer.py +10 -0
  251. synth_ai/tracing_v3/turso/daemon.py +2 -2
  252. synth_ai/tracing_v3/turso/native_manager.py +108 -77
  253. synth_ai/tracing_v3/utils.py +1 -1
  254. synth_ai/tui/__init__.py +5 -0
  255. synth_ai/tui/__main__.py +13 -0
  256. synth_ai/tui/cli/__init__.py +1 -0
  257. synth_ai/tui/cli/query_experiments.py +164 -0
  258. synth_ai/tui/cli/query_experiments_v3.py +164 -0
  259. synth_ai/tui/dashboard.py +911 -0
  260. synth_ai/utils/__init__.py +101 -0
  261. synth_ai/utils/base_url.py +94 -0
  262. synth_ai/utils/cli.py +131 -0
  263. synth_ai/utils/env.py +287 -0
  264. synth_ai/utils/http.py +169 -0
  265. synth_ai/utils/modal.py +308 -0
  266. synth_ai/utils/process.py +212 -0
  267. synth_ai/utils/prompts.py +39 -0
  268. synth_ai/utils/sqld.py +122 -0
  269. synth_ai/utils/task_app_discovery.py +882 -0
  270. synth_ai/utils/task_app_env.py +186 -0
  271. synth_ai/utils/task_app_state.py +318 -0
  272. synth_ai/utils/user_config.py +137 -0
  273. synth_ai/v0/config/__init__.py +1 -5
  274. synth_ai/v0/config/base_url.py +1 -7
  275. synth_ai/v0/tracing/config.py +1 -1
  276. synth_ai/v0/tracing/decorators.py +1 -1
  277. synth_ai/v0/tracing/upload.py +1 -1
  278. synth_ai/v0/tracing_v1/config.py +1 -1
  279. synth_ai/v0/tracing_v1/decorators.py +1 -1
  280. synth_ai/v0/tracing_v1/upload.py +1 -1
  281. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/METADATA +85 -31
  282. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/RECORD +286 -135
  283. synth_ai/cli/man.py +0 -106
  284. synth_ai/compound/cais.py +0 -0
  285. synth_ai/core/experiment.py +0 -13
  286. synth_ai/core/system.py +0 -15
  287. synth_ai/demo_registry.py +0 -295
  288. synth_ai/handshake.py +0 -109
  289. synth_ai/http.py +0 -26
  290. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/WHEEL +0 -0
  291. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/entry_points.txt +0 -0
  292. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/licenses/LICENSE +0 -0
  293. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,186 @@
1
+ import os
2
+
3
+ import click
4
+
5
+ from .base_url import PROD_BASE_URL_DEFAULT
6
+ from .env import mask_str, resolve_env_var
7
+ from .process import ensure_local_port_available
8
+ from .task_app_state import persist_env_api_key
9
+ from .user_config import load_user_env, update_user_config
10
+
11
+ __all__ = [
12
+ "ensure_env_credentials",
13
+ "ensure_port_free",
14
+ "preflight_env_key",
15
+ ]
16
+
17
+
18
+ def ensure_env_credentials(*, require_synth: bool = False, prompt: bool = True) -> None:
19
+ """Ensure required API keys are present in the process environment."""
20
+
21
+ load_user_env(override=False)
22
+
23
+ env_key = (os.environ.get("ENVIRONMENT_API_KEY") or "").strip()
24
+ if prompt and not env_key:
25
+ resolve_env_var("ENVIRONMENT_API_KEY")
26
+ env_key = (os.environ.get("ENVIRONMENT_API_KEY") or "").strip()
27
+
28
+ if env_key:
29
+ update_user_config(
30
+ {
31
+ "ENVIRONMENT_API_KEY": env_key,
32
+ "DEV_ENVIRONMENT_API_KEY": env_key,
33
+ }
34
+ )
35
+ persist_env_api_key(env_key)
36
+ elif prompt:
37
+ raise click.ClickException("ENVIRONMENT_API_KEY is required.")
38
+
39
+ synth_key = (os.environ.get("SYNTH_API_KEY") or "").strip()
40
+ if prompt and (require_synth or not synth_key):
41
+ resolve_env_var("SYNTH_API_KEY")
42
+ synth_key = (os.environ.get("SYNTH_API_KEY") or "").strip()
43
+
44
+ if synth_key:
45
+ update_user_config({"SYNTH_API_KEY": synth_key})
46
+ elif require_synth and prompt:
47
+ raise click.ClickException("SYNTH_API_KEY is required.")
48
+
49
+
50
+ def ensure_port_free(port: int, host: str, *, force: bool) -> None:
51
+ """Ensure a TCP port is not in use, optionally killing processes when ``force`` is True."""
52
+
53
+ if ensure_local_port_available(host, port, force=force):
54
+ return
55
+
56
+ message = f"Port {port} is still in use. Stop the running server and try again."
57
+ if force:
58
+ raise click.ClickException(message)
59
+ raise click.ClickException(f"Port {port} appears to be in use. Restart with --force to terminate it.")
60
+
61
+
62
+ def preflight_env_key(*, crash_on_failure: bool = False) -> None:
63
+ """Ensure ENVIRONMENT_API_KEY exists and attempt a backend registration."""
64
+
65
+ ensure_env_credentials(require_synth=False, prompt=not crash_on_failure)
66
+ load_user_env(override=False)
67
+
68
+ raw_backend = (
69
+ os.environ.get("BACKEND_BASE_URL")
70
+ or os.environ.get("SYNTH_BASE_URL")
71
+ or f"{PROD_BASE_URL_DEFAULT}/api"
72
+ )
73
+ backend_base = raw_backend.rstrip("/")
74
+ if not backend_base.endswith("/api"):
75
+ backend_base += "/api"
76
+
77
+ synth_key = os.environ.get("SYNTH_API_KEY") or ""
78
+ env_api_key = (os.environ.get("ENVIRONMENT_API_KEY") or "").strip()
79
+
80
+ def _mint_key() -> str | None:
81
+ try:
82
+ from synth_ai.learning.rl.secrets import mint_environment_api_key
83
+
84
+ key = mint_environment_api_key()
85
+ os.environ["ENVIRONMENT_API_KEY"] = key
86
+ os.environ.setdefault("DEV_ENVIRONMENT_API_KEY", key)
87
+ update_user_config(
88
+ {
89
+ "ENVIRONMENT_API_KEY": key,
90
+ "DEV_ENVIRONMENT_API_KEY": key,
91
+ }
92
+ )
93
+ persist_env_api_key(key)
94
+ click.echo(f"[preflight] minted ENVIRONMENT_API_KEY ({mask_str(key)})")
95
+ return key
96
+ except Exception as exc: # pragma: no cover - defensive fallback
97
+ if crash_on_failure:
98
+ raise click.ClickException(
99
+ f"[CRITICAL] Failed to mint ENVIRONMENT_API_KEY: {exc}"
100
+ ) from exc
101
+ click.echo(
102
+ f"[WARN] Failed to mint ENVIRONMENT_API_KEY automatically ({exc}); proceeding without upload"
103
+ )
104
+ return None
105
+
106
+ minted = False
107
+ if not env_api_key:
108
+ env_api_key = _mint_key() or ""
109
+ minted = bool(env_api_key)
110
+
111
+ if not env_api_key:
112
+ if crash_on_failure:
113
+ raise click.ClickException(
114
+ "[CRITICAL] ENVIRONMENT_API_KEY missing; run `synth-ai setup` to configure it."
115
+ )
116
+ click.echo("[preflight] ENVIRONMENT_API_KEY missing; continuing without verification.")
117
+ return
118
+
119
+ if minted:
120
+ persist_env_api_key(env_api_key)
121
+
122
+ if not synth_key.strip():
123
+ click.echo("[preflight] SYNTH_API_KEY not set; skipping backend preflight.")
124
+ return
125
+
126
+ try:
127
+ import base64
128
+
129
+ import httpx
130
+ from nacl.public import PublicKey, SealedBox
131
+ except Exception: # pragma: no cover - optional deps
132
+ click.echo("[preflight] Optional crypto dependencies missing; skipping upload.")
133
+ return
134
+
135
+ try:
136
+ with httpx.Client(timeout=15.0, headers={"Authorization": f"Bearer {synth_key}"}) as client:
137
+ click.echo(f"[preflight] backend={backend_base}")
138
+ click.echo("[preflight] fetching public key…")
139
+ rpk = client.get(f"{backend_base.rstrip('/')}/v1/crypto/public-key")
140
+ if rpk.status_code != 200:
141
+ click.echo(f"[preflight] public key fetch failed with {rpk.status_code}; skipping upload")
142
+ return
143
+ pk = (rpk.json() or {}).get("public_key")
144
+ if not pk:
145
+ click.echo("[preflight] no public key returned; skipping upload")
146
+ return
147
+
148
+ pk_bytes = base64.b64decode(pk, validate=True)
149
+ sealed_box = SealedBox(PublicKey(pk_bytes))
150
+ ciphertext = sealed_box.encrypt(env_api_key.encode("utf-8"))
151
+ ct_b64 = base64.b64encode(ciphertext).decode()
152
+ payload = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ct_b64}
153
+
154
+ click.echo(f"[preflight] posting to {backend_base.rstrip('/')}/v1/env-keys")
155
+ response = client.post(f"{backend_base.rstrip('/')}/v1/env-keys", json=payload)
156
+ if 200 <= response.status_code < 300:
157
+ click.echo(
158
+ f"✅ ENVIRONMENT_API_KEY uploaded successfully ({mask_str(env_api_key)})"
159
+ )
160
+ try:
161
+ ver = client.get(f"{backend_base.rstrip('/')}/v1/env-keys/verify")
162
+ if ver.status_code == 200 and (ver.json() or {}).get("present"):
163
+ click.echo("✅ Key verified in backend")
164
+ else:
165
+ click.echo(
166
+ f"⚠️ Verification returned {ver.status_code}, but upload succeeded - proceeding"
167
+ )
168
+ except Exception as verify_err: # pragma: no cover - verification optional
169
+ click.echo(
170
+ f"⚠️ Verification check failed ({verify_err}), but upload succeeded - proceeding"
171
+ )
172
+ return
173
+
174
+ snippet = response.text[:400] if response.text else ""
175
+ message = (
176
+ f"ENVIRONMENT_API_KEY upload failed with status {response.status_code}"
177
+ + (f" body={snippet}" if snippet else "")
178
+ )
179
+ if crash_on_failure:
180
+ raise click.ClickException(f"[CRITICAL] {message}")
181
+ click.echo(f"[WARN] {message}; proceeding anyway")
182
+ except Exception as exc: # pragma: no cover - network failures
183
+ message = f"Backend preflight for ENVIRONMENT_API_KEY failed: {exc}"
184
+ if crash_on_failure:
185
+ raise click.ClickException(f"[CRITICAL] {message}") from exc
186
+ click.echo(f"[WARN] {message}; proceeding anyway")
@@ -0,0 +1,318 @@
1
+ import json
2
+ import os
3
+ from collections.abc import Callable
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ DEFAULT_TASK_APP_SECRET_NAME = "synth-demo-task-app-secret"
9
+
10
+ __all__ = [
11
+ "DEFAULT_TASK_APP_SECRET_NAME",
12
+ "current_task_app_id",
13
+ "load_demo_dir",
14
+ "load_template_id",
15
+ "now_iso",
16
+ "persist_api_key",
17
+ "persist_demo_dir",
18
+ "persist_env_api_key",
19
+ "persist_task_url",
20
+ "persist_template_id",
21
+ "read_task_app_config",
22
+ "record_task_app",
23
+ "resolve_task_app_entry",
24
+ "task_app_config_path",
25
+ "task_app_id_from_path",
26
+ "update_task_app_entry",
27
+ "write_task_app_config",
28
+ ]
29
+
30
+
31
+ def task_app_config_path() -> str:
32
+ return os.path.expanduser("~/.synth-ai/task_app_config.json")
33
+
34
+
35
+ def read_task_app_config() -> dict[str, Any]:
36
+ path = task_app_config_path()
37
+ try:
38
+ if os.path.isfile(path):
39
+ with open(path, encoding="utf-8") as handle:
40
+ loaded = json.load(handle) or {}
41
+ if isinstance(loaded, dict):
42
+ apps = loaded.get("apps")
43
+ if isinstance(apps, dict):
44
+ return apps
45
+ except Exception:
46
+ pass
47
+ return {}
48
+
49
+
50
+ def write_task_app_config(apps: dict[str, Any]) -> None:
51
+ payload = {"apps": apps}
52
+ try:
53
+ path = task_app_config_path()
54
+ os.makedirs(os.path.dirname(path), exist_ok=True)
55
+ with open(path, "w", encoding="utf-8") as handle:
56
+ json.dump(payload, handle, indent=2, sort_keys=True)
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ def now_iso() -> str:
62
+ return (
63
+ datetime.now(UTC)
64
+ .replace(microsecond=0)
65
+ .isoformat()
66
+ .replace("+00:00", "Z")
67
+ )
68
+
69
+
70
+ def task_app_id_from_path(path: str | Path | None) -> str | None:
71
+ if not path:
72
+ return None
73
+ try:
74
+ return str(Path(path).expanduser().resolve())
75
+ except Exception:
76
+ return None
77
+
78
+
79
+ def current_task_app_id() -> str | None:
80
+ try:
81
+ return str(Path.cwd().resolve())
82
+ except Exception:
83
+ return None
84
+
85
+
86
+ def update_task_app_entry(
87
+ path: str | Path | None,
88
+ *,
89
+ template_id: str | None = None,
90
+ mutate: Callable[[dict[str, Any]], None] | None = None,
91
+ ) -> dict[str, Any]:
92
+ task_id = task_app_id_from_path(path)
93
+ if task_id is None:
94
+ task_id = current_task_app_id()
95
+ if task_id is None:
96
+ return {}
97
+
98
+ apps = read_task_app_config()
99
+ now = now_iso()
100
+ entry = apps.get(task_id)
101
+ if entry is None:
102
+ entry = {
103
+ "task_app_path": task_id,
104
+ "template_id": template_id,
105
+ "created_at": now,
106
+ "last_used": now,
107
+ "modal": {
108
+ "app_name": None,
109
+ "base_url": None,
110
+ "secret_name": DEFAULT_TASK_APP_SECRET_NAME,
111
+ "created_at": None,
112
+ "last_used": None,
113
+ },
114
+ }
115
+ apps[task_id] = entry
116
+ else:
117
+ entry.setdefault(
118
+ "modal",
119
+ {
120
+ "app_name": None,
121
+ "base_url": None,
122
+ "secret_name": DEFAULT_TASK_APP_SECRET_NAME,
123
+ "created_at": None,
124
+ "last_used": None,
125
+ },
126
+ )
127
+ if template_id is not None:
128
+ entry["template_id"] = template_id
129
+
130
+ if mutate is not None:
131
+ mutate(entry)
132
+
133
+ entry["last_used"] = now
134
+ write_task_app_config(apps)
135
+ return entry
136
+
137
+
138
+ def record_task_app(
139
+ path: str,
140
+ *,
141
+ template_id: str | None = None,
142
+ secret_name: str | None = None,
143
+ ) -> None:
144
+
145
+ def _mutate(entry: dict[str, Any]) -> None:
146
+ if secret_name:
147
+ modal_block = entry.setdefault(
148
+ "modal",
149
+ {
150
+ "app_name": None,
151
+ "base_url": None,
152
+ "secret_name": DEFAULT_TASK_APP_SECRET_NAME,
153
+ "created_at": None,
154
+ "last_used": None,
155
+ },
156
+ )
157
+ modal_block["secret_name"] = secret_name
158
+
159
+ update_task_app_entry(path, template_id=template_id, mutate=_mutate if secret_name else None)
160
+
161
+
162
+ def _select_entry(
163
+ *,
164
+ preferred_path: str | None = None,
165
+ predicate: Callable[[dict[str, Any]], bool] | None = None,
166
+ ) -> tuple[str | None, dict[str, Any]]:
167
+ entries = read_task_app_config()
168
+ if not entries:
169
+ return None, {}
170
+
171
+ def _matches(entry: dict[str, Any]) -> bool:
172
+ return predicate(entry) if predicate is not None else True
173
+
174
+ if preferred_path:
175
+ entry = entries.get(preferred_path)
176
+ if isinstance(entry, dict) and _matches(entry):
177
+ return preferred_path, entry
178
+
179
+ try:
180
+ cwd = str(Path.cwd().resolve())
181
+ entry = entries.get(cwd)
182
+ if isinstance(entry, dict) and _matches(entry):
183
+ return cwd, entry
184
+ except Exception:
185
+ pass
186
+
187
+ best_path: str | None = None
188
+ best_entry: dict[str, Any] = {}
189
+ best_ts = ""
190
+ for path, entry in entries.items():
191
+ if not isinstance(entry, dict) or not _matches(entry):
192
+ continue
193
+ ts = str(entry.get("last_used") or "")
194
+ if ts > best_ts:
195
+ best_ts = ts
196
+ best_path = path
197
+ best_entry = entry
198
+ return best_path, best_entry
199
+
200
+
201
+ def resolve_task_app_entry(
202
+ preferred_path: str | None = None,
203
+ *,
204
+ predicate: Callable[[dict[str, Any]], bool] | None = None,
205
+ ) -> tuple[str | None, dict[str, Any]]:
206
+ path, entry = _select_entry(preferred_path=preferred_path, predicate=predicate)
207
+ return path, entry if isinstance(entry, dict) else {}
208
+
209
+
210
+ def persist_demo_dir(demo_dir: str) -> None:
211
+ def _mutate(entry: dict[str, Any]) -> None:
212
+ entry["is_demo"] = True
213
+
214
+ update_task_app_entry(demo_dir, mutate=_mutate)
215
+
216
+
217
+ def load_demo_dir() -> str | None:
218
+ path, _ = _select_entry(predicate=lambda entry: entry.get("is_demo") or entry.get("template_id"))
219
+ return path
220
+
221
+
222
+ def persist_template_id(template_id: str | None) -> None:
223
+ demo_dir = load_demo_dir() or current_task_app_id()
224
+ if demo_dir is None:
225
+ return
226
+ update_task_app_entry(demo_dir, template_id=template_id)
227
+
228
+
229
+ def load_template_id() -> str | None:
230
+ _, entry = _select_entry(predicate=lambda item: item.get("template_id"))
231
+ value = entry.get("template_id") if isinstance(entry, dict) else None
232
+ return str(value) if isinstance(value, str) else None
233
+
234
+
235
+ def persist_api_key(key: str) -> None:
236
+ target = load_demo_dir() or current_task_app_id()
237
+
238
+ def _mutate(entry: dict[str, Any]) -> None:
239
+ secrets = entry.setdefault("secrets", {})
240
+ secrets["synth_api_key"] = key
241
+
242
+ update_task_app_entry(target, mutate=_mutate)
243
+
244
+
245
+ def persist_env_api_key(key: str, path: str | Path | None = None) -> None:
246
+ target = path or load_demo_dir()
247
+
248
+ def _mutate(entry: dict[str, Any]) -> None:
249
+ secrets = entry.setdefault("secrets", {})
250
+ secrets["environment_api_key"] = key
251
+
252
+ update_task_app_entry(target, mutate=_mutate)
253
+
254
+
255
+ def _derive_modal_app_name(url: str | None) -> str | None:
256
+ if not url:
257
+ return None
258
+ try:
259
+ from urllib.parse import urlparse
260
+
261
+ host = urlparse(url).hostname or ""
262
+ if "--" not in host:
263
+ return None
264
+ suffix = host.split("--", 1)[1]
265
+ core = suffix.split(".modal", 1)[0]
266
+ if core.endswith("-fastapi-app"):
267
+ core = core[: -len("-fastapi-app")]
268
+ return core.strip() or None
269
+ except Exception:
270
+ return None
271
+
272
+
273
+ def persist_task_url(url: str, *, name: str | None = None, path: str | None = None) -> None:
274
+ normalized_url = (url or "").rstrip("/")
275
+ task_id = task_app_id_from_path(path) or current_task_app_id()
276
+ if task_id is None:
277
+ return
278
+
279
+ existing = read_task_app_config().get(task_id, {})
280
+ previous_modal = dict(existing.get("modal", {})) if isinstance(existing, dict) else {}
281
+
282
+ derived_name = name or _derive_modal_app_name(normalized_url)
283
+
284
+ def _mutate(entry: dict[str, Any]) -> None:
285
+ modal_block = entry.setdefault(
286
+ "modal",
287
+ {
288
+ "app_name": None,
289
+ "base_url": None,
290
+ "secret_name": DEFAULT_TASK_APP_SECRET_NAME,
291
+ "created_at": None,
292
+ "last_used": None,
293
+ },
294
+ )
295
+ now = now_iso()
296
+ if modal_block.get("created_at") is None:
297
+ modal_block["created_at"] = now
298
+ modal_block["last_used"] = now
299
+ modal_block["base_url"] = normalized_url
300
+ if derived_name:
301
+ modal_block["app_name"] = derived_name
302
+ modal_block["secret_name"] = DEFAULT_TASK_APP_SECRET_NAME
303
+
304
+ entry = update_task_app_entry(path or task_id, mutate=_mutate)
305
+
306
+ modal_after = entry.get("modal", {}) if isinstance(entry, dict) else {}
307
+ changed: list[str] = []
308
+ if previous_modal.get("base_url") != modal_after.get("base_url"):
309
+ changed.append("TASK_APP_BASE_URL")
310
+ if derived_name and previous_modal.get("app_name") != modal_after.get("app_name"):
311
+ changed.append("TASK_APP_NAME")
312
+ if previous_modal.get("secret_name") != modal_after.get("secret_name"):
313
+ changed.append("TASK_APP_SECRET_NAME")
314
+
315
+ if changed:
316
+ print(f"Saved {', '.join(changed)} to {task_app_config_path()}")
317
+ if "TASK_APP_SECRET_NAME" in changed:
318
+ print(f"TASK_APP_SECRET_NAME={DEFAULT_TASK_APP_SECRET_NAME}")
@@ -0,0 +1,137 @@
1
+ import contextlib
2
+ import json
3
+ import os
4
+ from collections.abc import Mapping
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ CONFIG_DIR = Path(os.path.expanduser("~/.synth-ai"))
9
+ USER_CONFIG_PATH = CONFIG_DIR / "user_config.json"
10
+
11
+
12
+ def _ensure_config_dir() -> None:
13
+ with contextlib.suppress(Exception):
14
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+
17
+ def load_user_config() -> dict[str, Any]:
18
+ """Return the persisted user config as a dict (empty if missing or invalid)."""
19
+
20
+ try:
21
+ if USER_CONFIG_PATH.is_file():
22
+ with USER_CONFIG_PATH.open("r", encoding="utf-8") as fh:
23
+ data = json.load(fh)
24
+ return data if isinstance(data, dict) else {}
25
+ except Exception:
26
+ return {}
27
+ return {}
28
+
29
+
30
+ def save_user_config(config: Mapping[str, Any]) -> None:
31
+ """Persist a new user config dictionary (overwrites previous contents)."""
32
+
33
+ _ensure_config_dir()
34
+ try:
35
+ with USER_CONFIG_PATH.open("w", encoding="utf-8") as fh:
36
+ json.dump(dict(config), fh, indent=2, sort_keys=True)
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ def update_user_config(updates: Mapping[str, Any]) -> dict[str, Any]:
42
+ """Merge `updates` into the existing user config and persist the result."""
43
+
44
+ current = load_user_config()
45
+ current.update(updates)
46
+ save_user_config(current)
47
+ return current
48
+
49
+
50
+ def _load_json(path: Path) -> dict[str, Any]:
51
+ try:
52
+ if path.is_file():
53
+ with path.open("r", encoding="utf-8") as handle:
54
+ data = json.load(handle)
55
+ return data if isinstance(data, dict) else {}
56
+ except Exception:
57
+ return {}
58
+ return {}
59
+
60
+
61
+ def _load_task_app_entries() -> dict[str, Any]:
62
+ data = _load_json(Path(os.path.expanduser("~/.synth-ai/task_app_config.json")))
63
+ if "apps" in data and isinstance(data["apps"], dict):
64
+ return data["apps"]
65
+ return {}
66
+
67
+
68
+ def _select_task_app_entry(entries: dict[str, Any]) -> tuple[str | None, dict[str, Any]]:
69
+ if not entries:
70
+ return None, {}
71
+
72
+ demo_dir = os.environ.get("SYNTH_DEMO_DIR")
73
+ if demo_dir and demo_dir in entries:
74
+ return demo_dir, entries[demo_dir]
75
+
76
+ try:
77
+ cwd = str(Path.cwd().resolve())
78
+ if cwd in entries:
79
+ return cwd, entries[cwd]
80
+ except Exception:
81
+ pass
82
+
83
+ best_key = None
84
+ best_entry: dict[str, Any] = {}
85
+ best_ts = ""
86
+ for key, entry in entries.items():
87
+ if not isinstance(entry, dict):
88
+ continue
89
+ ts = str(entry.get("last_used") or "")
90
+ if ts > best_ts:
91
+ best_key = key
92
+ best_entry = entry
93
+ best_ts = ts
94
+ return best_key, best_entry
95
+
96
+
97
+ def load_user_env(*, override: bool = True) -> dict[str, str]:
98
+ """Hydrate ``os.environ`` from persisted Synth SDK state."""
99
+
100
+ applied: dict[str, str] = {}
101
+
102
+ def _apply(mapping: Mapping[str, Any]) -> None:
103
+ for key, value in mapping.items():
104
+ if value is None:
105
+ continue
106
+ str_value = value if isinstance(value, str) else str(value)
107
+ if override or key not in os.environ:
108
+ os.environ[key] = str_value
109
+ applied[key] = str_value
110
+
111
+ config = load_user_config()
112
+ _apply(config)
113
+
114
+ entry_key, entry = _select_task_app_entry(_load_task_app_entries())
115
+ if entry:
116
+ modal_block = entry.get("modal") if isinstance(entry.get("modal"), dict) else {}
117
+ if modal_block:
118
+ _apply(
119
+ {
120
+ "TASK_APP_BASE_URL": modal_block.get("base_url"),
121
+ "TASK_APP_NAME": modal_block.get("app_name"),
122
+ "TASK_APP_SECRET_NAME": modal_block.get("secret_name"),
123
+ }
124
+ )
125
+ if entry_key:
126
+ _apply({"SYNTH_DEMO_DIR": entry_key})
127
+
128
+ return applied
129
+
130
+
131
+ __all__ = [
132
+ "USER_CONFIG_PATH",
133
+ "load_user_config",
134
+ "save_user_config",
135
+ "update_user_config",
136
+ "load_user_env",
137
+ ]
@@ -3,13 +3,9 @@ from __future__ import annotations
3
3
  # Compatibility package to mirror historical import paths.
4
4
  # Re-export constants from the modern location under synth_ai.config.
5
5
 
6
- try:
7
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT as _PROD
8
- except Exception: # pragma: no cover
9
- _PROD = None
6
+ from synth_ai._utils.base_url import PROD_BASE_URL_DEFAULT as _PROD
10
7
 
11
8
  __all__ = [
12
9
  "_PROD",
13
10
  ]
14
11
 
15
-
@@ -1,12 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- try:
4
- # Prefer the modern constant
5
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT as PROD_BASE_URL_DEFAULT # type: ignore
6
- except Exception: # pragma: no cover
7
- # Fallback if the modern module moves; provide a safe default
8
- PROD_BASE_URL_DEFAULT = "https://agent-learning.onrender.com"
3
+ from synth_ai._utils.base_url import PROD_BASE_URL_DEFAULT
9
4
 
10
5
  __all__ = ["PROD_BASE_URL_DEFAULT"]
11
6
 
12
-
@@ -12,7 +12,7 @@ from opentelemetry.sdk.trace.export import (
12
12
  )
13
13
  from pydantic import BaseModel, ConfigDict, Field
14
14
 
15
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
15
+ from synth_ai._utils.base_url import PROD_BASE_URL_DEFAULT
16
16
 
17
17
 
18
18
  class InMemoryExporter(SpanExporter):
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, ParamSpec, TypeVar, Union
10
10
  if TYPE_CHECKING:
11
11
  from .trackers import SynthTrackerAsync, SynthTrackerSync
12
12
 
13
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
13
+ from synth_ai._utils.base_url import PROD_BASE_URL_DEFAULT
14
14
 
15
15
  from .abstractions import (
16
16
  AgentComputeStep,
@@ -13,7 +13,7 @@ from pydantic import BaseModel, ConfigDict, field_validator
13
13
  from requests.adapters import HTTPAdapter
14
14
  from urllib3.poolmanager import PoolManager
15
15
 
16
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
16
+ from synth_ai._utils.base_url import PROD_BASE_URL_DEFAULT
17
17
 
18
18
  from .abstractions import Dataset, SystemTrace
19
19
  from .events.store import event_store