synth-ai 0.2.9.dev7__py3-none-any.whl → 0.2.10__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 (323) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +8 -11
  3. examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
  4. examples/multi_step/crafter_rl_lora.md +29 -0
  5. examples/qwen_coder/README.md +102 -0
  6. examples/qwen_coder/_shared.py +113 -0
  7. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  8. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  9. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  10. examples/qwen_coder/generate_dataset.py +98 -0
  11. examples/qwen_coder/infer_ft_smoke.py +65 -0
  12. examples/qwen_coder/infer_prod_proxy.py +73 -0
  13. examples/qwen_coder/infer_via_synth.py +87 -0
  14. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  15. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  16. examples/qwen_coder/sft_full_17b.py +103 -0
  17. examples/qwen_coder/sft_lora_30b.py +110 -0
  18. examples/qwen_coder/subset_jsonl.py +39 -0
  19. examples/qwen_coder/todos.md +38 -0
  20. examples/qwen_coder/validate_jsonl.py +60 -0
  21. examples/rl/run_eval.py +36 -37
  22. examples/rl/run_rl_and_save.py +5 -5
  23. examples/rl/task_app/math_single_step.py +65 -43
  24. examples/rl/task_app/math_task_app.py +3 -3
  25. examples/sft/README.md +139 -0
  26. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  27. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  28. examples/sft/evaluate.py +117 -0
  29. examples/sft/export_dataset.py +117 -0
  30. examples/sft/generate_traces.py +162 -0
  31. examples/swe/__init__.py +12 -0
  32. examples/swe/task_app/README.md +105 -0
  33. examples/swe/task_app/__init__.py +2 -0
  34. examples/swe/task_app/grpo_swe_mini.py +571 -0
  35. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  36. examples/swe/task_app/hosted/README.md +173 -0
  37. examples/swe/task_app/hosted/__init__.py +5 -0
  38. examples/swe/task_app/hosted/branching.py +143 -0
  39. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  40. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  41. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  42. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  43. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  44. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  45. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  46. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  47. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  48. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  49. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  50. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  53. examples/swe/task_app/hosted/hosted_app.py +204 -0
  54. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  55. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  56. examples/swe/task_app/hosted/main.py +100 -0
  57. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  58. examples/swe/task_app/hosted/registry.py +195 -0
  59. examples/swe/task_app/hosted/rollout.py +1869 -0
  60. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  61. examples/swe/task_app/hosted/storage/volume.py +211 -0
  62. examples/swe/task_app/hosted/test_agents.py +161 -0
  63. examples/swe/task_app/hosted/test_service.py +137 -0
  64. examples/swe/task_app/hosted/utils.py +62 -0
  65. examples/vlm/PROPOSAL.md +53 -0
  66. examples/vlm/README.md +68 -0
  67. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  68. examples/vlm/crafter_image_only_agent.py +207 -0
  69. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  70. examples/vlm/filter_image_rows.py +63 -0
  71. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  72. examples/warming_up_to_rl/analyze_trace_db.py +5 -5
  73. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  74. examples/warming_up_to_rl/export_trace_sft.py +78 -21
  75. examples/warming_up_to_rl/groq_test.py +4 -4
  76. examples/warming_up_to_rl/manage_secrets.py +13 -18
  77. examples/warming_up_to_rl/run_eval.py +42 -44
  78. examples/warming_up_to_rl/run_fft_and_save.py +11 -16
  79. examples/warming_up_to_rl/run_local_rollout.py +1 -3
  80. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -4
  81. examples/warming_up_to_rl/run_local_rollout_parallel.py +1 -4
  82. examples/warming_up_to_rl/run_local_rollout_traced.py +3 -5
  83. examples/warming_up_to_rl/run_rl_and_save.py +5 -6
  84. examples/warming_up_to_rl/run_rollout_remote.py +8 -10
  85. examples/warming_up_to_rl/task_app/README.md +6 -2
  86. examples/warming_up_to_rl/task_app/grpo_crafter.py +234 -35
  87. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +2 -3
  88. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  89. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  90. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +131 -114
  91. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +101 -41
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +73 -51
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +14 -6
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +16 -16
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +32 -34
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +94 -31
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +303 -203
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +328 -225
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +13 -13
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +1 -0
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  105. synth_ai/api/models/supported.py +376 -0
  106. synth_ai/api/train/builders.py +128 -21
  107. synth_ai/api/train/cli.py +80 -64
  108. synth_ai/api/train/config_finder.py +7 -2
  109. synth_ai/api/train/env_resolver.py +1 -1
  110. synth_ai/api/train/pollers.py +2 -1
  111. synth_ai/api/train/supported_algos.py +139 -0
  112. synth_ai/api/train/task_app.py +1 -2
  113. synth_ai/api/train/utils.py +13 -44
  114. synth_ai/cli/__init__.py +8 -0
  115. synth_ai/cli/_modal_wrapper.py +28 -0
  116. synth_ai/cli/_typer_patch.py +49 -0
  117. synth_ai/cli/balance.py +1 -2
  118. synth_ai/cli/calc.py +1 -1
  119. synth_ai/cli/demo.py +2 -1
  120. synth_ai/cli/recent.py +2 -2
  121. synth_ai/cli/rl_demo.py +2 -1
  122. synth_ai/cli/root.py +11 -13
  123. synth_ai/cli/status.py +2 -2
  124. synth_ai/cli/task_apps.py +529 -179
  125. synth_ai/cli/traces.py +6 -4
  126. synth_ai/cli/watch.py +12 -18
  127. synth_ai/demo_registry.py +1 -1
  128. synth_ai/demos/core/cli.py +36 -43
  129. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  130. synth_ai/demos/demo_task_apps/core.py +17 -25
  131. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +3 -4
  132. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  133. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -4
  134. synth_ai/demos/demo_task_apps/math/modal_task_app.py +16 -18
  135. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
  136. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  137. synth_ai/environments/reproducibility/tree.py +2 -5
  138. synth_ai/environments/service/app.py +11 -12
  139. synth_ai/environments/service/core_routes.py +4 -7
  140. synth_ai/environments/stateful/engine.py +1 -1
  141. synth_ai/environments/tasks/core.py +1 -0
  142. synth_ai/environments/tasks/filters.py +5 -6
  143. synth_ai/environments/tasks/utils.py +4 -5
  144. synth_ai/handshake.py +9 -9
  145. synth_ai/http.py +1 -1
  146. synth_ai/http_client.py +18 -10
  147. synth_ai/inference/client.py +15 -5
  148. synth_ai/jobs/client.py +78 -83
  149. synth_ai/learning/__init__.py +41 -6
  150. synth_ai/learning/algorithms.py +14 -0
  151. synth_ai/learning/client.py +91 -24
  152. synth_ai/learning/config.py +2 -38
  153. synth_ai/learning/ft_client.py +4 -59
  154. synth_ai/learning/health.py +5 -6
  155. synth_ai/learning/jobs.py +31 -47
  156. synth_ai/{rl → learning/rl}/__init__.py +14 -4
  157. synth_ai/learning/rl/client.py +267 -0
  158. synth_ai/learning/rl/config.py +31 -0
  159. synth_ai/{rl → learning/rl}/contracts.py +5 -8
  160. synth_ai/{rl → learning/rl}/env_keys.py +39 -15
  161. synth_ai/learning/rl/secrets.py +13 -0
  162. synth_ai/learning/rl_client.py +2 -281
  163. synth_ai/learning/sft/__init__.py +29 -0
  164. synth_ai/learning/sft/client.py +68 -0
  165. synth_ai/learning/sft/config.py +270 -0
  166. synth_ai/learning/sft/data.py +295 -0
  167. synth_ai/learning/sse.py +25 -24
  168. synth_ai/learning/validators.py +25 -28
  169. synth_ai/lm/__init__.py +21 -47
  170. synth_ai/task/__init__.py +25 -27
  171. synth_ai/task/apps/__init__.py +7 -8
  172. synth_ai/task/auth.py +8 -8
  173. synth_ai/task/client.py +14 -14
  174. synth_ai/task/contracts.py +36 -35
  175. synth_ai/task/datasets.py +6 -5
  176. synth_ai/task/errors.py +10 -10
  177. synth_ai/task/health.py +17 -9
  178. synth_ai/task/json.py +58 -23
  179. synth_ai/task/proxy.py +13 -9
  180. synth_ai/task/rubrics.py +16 -15
  181. synth_ai/task/server.py +12 -12
  182. synth_ai/task/tracing_utils.py +4 -4
  183. synth_ai/task/vendors.py +5 -6
  184. synth_ai/tracing_v3/__init__.py +2 -0
  185. synth_ai/tracing_v3/abstractions.py +21 -4
  186. synth_ai/tracing_v3/decorators.py +18 -16
  187. synth_ai/tracing_v3/hooks.py +5 -5
  188. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  189. synth_ai/tracing_v3/session_tracer.py +40 -14
  190. synth_ai/tracing_v3/storage/base.py +85 -0
  191. synth_ai/tracing_v3/storage/config.py +21 -8
  192. synth_ai/tracing_v3/storage/factory.py +10 -7
  193. synth_ai/tracing_v3/storage/utils.py +4 -2
  194. synth_ai/tracing_v3/turso/daemon.py +7 -2
  195. synth_ai/tracing_v3/turso/models.py +2 -2
  196. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  197. synth_ai/tracing_v3/utils.py +4 -4
  198. synth_ai/v0/api/__init__.py +8 -0
  199. synth_ai/v0/api/models/__init__.py +8 -0
  200. synth_ai/v0/api/models/supported.py +8 -0
  201. synth_ai/v0/config/__init__.py +15 -0
  202. synth_ai/v0/config/base_url.py +12 -0
  203. synth_ai/v0/lm/__init__.py +51 -0
  204. synth_ai/{lm → v0/lm}/caching/ephemeral.py +2 -2
  205. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  206. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  207. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  208. synth_ai/{lm → v0/lm}/config.py +6 -1
  209. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  210. synth_ai/{lm → v0/lm}/core/main.py +6 -6
  211. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  212. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -14
  213. synth_ai/{lm → v0/lm}/core/vendor_clients.py +2 -2
  214. synth_ai/{lm → v0/lm}/overrides.py +2 -2
  215. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  216. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  217. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  218. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  219. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +9 -9
  220. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  221. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  222. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +10 -10
  223. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +8 -8
  224. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +2 -2
  225. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +3 -3
  226. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  227. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  228. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  229. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  230. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  231. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  232. synth_ai/{lm → v0/lm}/vendors/synth_client.py +1 -1
  233. synth_ai/v0/tracing_v3/__init__.py +10 -0
  234. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  235. synth_ai/v0/tracing_v3/decorators.py +3 -0
  236. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  237. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  238. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
  239. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/RECORD +269 -233
  240. examples/common_old/backend.py +0 -20
  241. examples/evals_old/README.md +0 -98
  242. examples/evals_old/__init__.py +0 -6
  243. examples/evals_old/compare_models.py +0 -1038
  244. examples/evals_old/example_log.md +0 -145
  245. examples/evals_old/run_demo.sh +0 -126
  246. examples/evals_old/trace_analysis.py +0 -270
  247. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  248. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  249. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  250. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -243
  251. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  252. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  253. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  254. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  255. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  256. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -119
  257. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  258. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  259. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -243
  260. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  261. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  262. examples/finetuning_old/synth_qwen_v1/infer.py +0 -36
  263. examples/finetuning_old/synth_qwen_v1/poll.py +0 -46
  264. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  265. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  266. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1933
  267. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -210
  268. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -237
  269. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  270. examples/finetuning_old/synth_qwen_v1/util.py +0 -152
  271. examples/rl_old/task_app.py +0 -1131
  272. synth_ai/experimental/synth_oss.py +0 -445
  273. synth_ai/learning/filtering.py +0 -0
  274. synth_ai/learning/offline/dpo.py +0 -0
  275. synth_ai/learning/offline/providers.py +0 -7
  276. synth_ai/learning/offline/sft.py +0 -0
  277. synth_ai/learning/offline/shared.py +0 -0
  278. synth_ai/learning/online/grpo.py +0 -0
  279. synth_ai/learning/online/irft.py +0 -0
  280. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  281. synth_ai/learning/prompts/gepa.py +0 -0
  282. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -211
  283. synth_ai/learning/prompts/mipro.py +0 -289
  284. synth_ai/learning/prompts/random_search.py +0 -249
  285. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  286. synth_ai/learning/prompts/run_random_search_banking77.py +0 -329
  287. synth_ai/rl/secrets.py +0 -19
  288. synth_ai/scripts/verify_rewards.py +0 -100
  289. synth_ai/tracing/__init__.py +0 -30
  290. synth_ai/tracing_v1/__init__.py +0 -33
  291. synth_ai/tracing_v3/turso/__init__.py +0 -25
  292. synth_ai/tracing_v3/turso/manager.py +0 -838
  293. synth_ai/zyk/__init__.py +0 -30
  294. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  295. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  296. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  297. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  298. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  299. /synth_ai/{lm → v0/lm}/core/exceptions.py +0 -0
  300. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  301. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  302. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  303. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  304. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  305. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  306. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  307. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  308. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  309. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  310. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  311. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  312. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  313. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  314. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  315. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  316. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  317. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  318. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  319. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  320. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
  321. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
  322. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
  323. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
@@ -1,40 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pathlib import Path
4
3
  import json
5
- from typing import Any, Dict
6
- from urllib.parse import urlparse
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from synth_ai.learning.sft import SFTDataError, parse_jsonl_line
7
8
 
8
9
 
9
10
  def validate_training_jsonl(path: str | Path, *, sample_lines: int = 50) -> None:
10
11
  p = Path(path)
11
12
  if not p.exists():
12
13
  raise FileNotFoundError(str(p))
13
- lines = p.read_text().splitlines()
14
- if not lines:
14
+
15
+ max_samples = max(1, sample_lines)
16
+ non_empty_lines = 0
17
+
18
+ with p.open("r", encoding="utf-8") as fh:
19
+ for lineno, raw_line in enumerate(fh, start=1):
20
+ stripped = raw_line.strip()
21
+ if not stripped:
22
+ continue
23
+ non_empty_lines += 1
24
+ if non_empty_lines > max_samples:
25
+ break
26
+ try:
27
+ parse_jsonl_line(stripped, min_messages=2)
28
+ except json.JSONDecodeError as exc:
29
+ raise ValueError(f"invalid json on line {lineno}: {exc}") from exc
30
+ except SFTDataError as exc:
31
+ raise ValueError(f"line {lineno}: {exc}") from exc
32
+
33
+ if non_empty_lines == 0:
15
34
  raise ValueError("empty JSONL")
16
- for i, line in enumerate(lines[: max(1, sample_lines)], start=1):
17
- if not line.strip():
18
- continue
19
- try:
20
- obj = json.loads(line)
21
- except Exception as e:
22
- raise ValueError(f"invalid json on line {i}: {e}") from e
23
- msgs = obj.get("messages")
24
- if not isinstance(msgs, list) or len(msgs) < 2:
25
- raise ValueError(f"line {i}: missing messages[] with at least 2 turns")
26
- roles = [m.get("role") for m in msgs if isinstance(m, dict)]
27
- if not roles or not isinstance(roles[0], str):
28
- raise ValueError(f"line {i}: missing first role")
29
- for m in msgs:
30
- if not isinstance(m, dict):
31
- raise ValueError(f"line {i}: non-dict message")
32
- if (
33
- not isinstance(m.get("role"), str)
34
- or not isinstance(m.get("content"), str)
35
- or not m["content"].strip()
36
- ):
37
- raise ValueError(f"line {i}: invalid role/content")
38
35
 
39
36
 
40
37
  def validate_task_app_url(url: str, *, name: str = "TASK_APP_BASE_URL") -> None:
@@ -43,7 +40,7 @@ def validate_task_app_url(url: str, *, name: str = "TASK_APP_BASE_URL") -> None:
43
40
  _vt(url, name=name)
44
41
 
45
42
 
46
- def validate_trainer_cfg_rl(trainer: Dict[str, Any]) -> None:
43
+ def validate_trainer_cfg_rl(trainer: dict[str, Any]) -> None:
47
44
  bs = int(trainer.get("batch_size", 1))
48
45
  gs = int(trainer.get("group_size", 2))
49
46
  if bs < 1:
synth_ai/lm/__init__.py CHANGED
@@ -1,51 +1,25 @@
1
- """
2
- Synth AI Language Model Interface.
1
+ """Deprecated shim forwarding to synth_ai.v0.lm."""
3
2
 
4
- Provides a unified interface for multiple LLM providers including OpenAI and Synth.
5
- """
3
+ import importlib as _importlib
4
+ import pkgutil as _pkgutil
5
+ import sys as _sys
6
+ from pathlib import Path as _Path
6
7
 
7
- from .config import OpenAIConfig, SynthConfig
8
- from .core.main_v3 import LM
9
- from .unified_interface import (
10
- OpenAIProvider,
11
- SynthProvider,
12
- UnifiedLMClient,
13
- UnifiedLMProvider,
14
- create_provider,
15
- )
16
- from .vendors.synth_client import (
17
- AsyncSynthClient,
18
- SyncSynthClient,
19
- create_async_client,
20
- create_chat_completion_async,
21
- create_chat_completion_sync,
22
- create_sync_client,
23
- )
24
- from .warmup import get_warmup_status, warmup_synth_model
8
+ _TARGET_PREFIX = "synth_ai.v0.lm"
9
+ _ALIAS_PREFIX = __name__
25
10
 
26
- __all__ = [
27
- # Configuration
28
- "SynthConfig",
29
- "OpenAIConfig",
30
- # Warmup utilities
31
- "warmup_synth_model",
32
- "get_warmup_status",
33
- # Unified interface
34
- "UnifiedLMProvider",
35
- "OpenAIProvider",
36
- "SynthProvider",
37
- "UnifiedLMClient",
38
- "create_provider",
39
- # Synth client
40
- "AsyncSynthClient",
41
- "SyncSynthClient",
42
- "create_async_client",
43
- "create_sync_client",
44
- "create_chat_completion_async",
45
- "create_chat_completion_sync",
46
- # Core LM class
47
- "LM",
48
- ]
11
+ _alias_path = _Path(__file__).resolve().parents[1] / "v0" / "lm"
12
+ __path__ = [str(_alias_path)] # type: ignore[assignment]
49
13
 
50
- # Version info
51
- __version__ = "0.1.0"
14
+ _pkg = _importlib.import_module(_TARGET_PREFIX)
15
+ _sys.modules[_ALIAS_PREFIX] = _pkg
16
+
17
+ for _finder, _name, _ispkg in _pkgutil.walk_packages(_pkg.__path__, prefix=_TARGET_PREFIX + "."): # type: ignore[attr-defined]
18
+ try:
19
+ _module = _importlib.import_module(_name)
20
+ except Exception: # pragma: no cover - best effort
21
+ continue
22
+ _alias = _ALIAS_PREFIX + _name[len(_TARGET_PREFIX) :]
23
+ _sys.modules[_alias] = _module
24
+
25
+ del _finder, _name, _ispkg, _module, _alias, _TARGET_PREFIX, _ALIAS_PREFIX, _alias_path
synth_ai/task/__init__.py CHANGED
@@ -1,59 +1,57 @@
1
- from .validators import validate_task_app_url
2
- from .health import task_app_health
1
+ from .auth import (
2
+ is_api_key_header_authorized,
3
+ normalize_environment_api_key,
4
+ require_api_key_dependency,
5
+ )
6
+ from .client import TaskAppClient
3
7
  from .contracts import (
4
- TaskAppContract,
5
- TaskAppEndpoints,
6
8
  RolloutEnvSpec,
9
+ RolloutMetrics,
7
10
  RolloutPolicySpec,
8
11
  RolloutRecordConfig,
9
- RolloutSafetyConfig,
10
12
  RolloutRequest,
11
13
  RolloutResponse,
12
- RolloutTrajectory,
14
+ RolloutSafetyConfig,
13
15
  RolloutStep,
14
- RolloutMetrics,
16
+ RolloutTrajectory,
17
+ TaskAppContract,
18
+ TaskAppEndpoints,
15
19
  TaskInfo,
16
20
  )
21
+ from .datasets import TaskDatasetRegistry, TaskDatasetSpec
22
+ from .errors import error_payload, http_exception, json_error_response
23
+ from .health import task_app_health
17
24
  from .json import to_jsonable
18
- from .auth import (
19
- normalize_environment_api_key,
20
- is_api_key_header_authorized,
21
- require_api_key_dependency,
22
- )
23
- from .vendors import (
24
- normalize_vendor_keys,
25
- get_openai_key_or_503,
26
- get_groq_key_or_503,
27
- )
28
25
  from .proxy import (
29
26
  INTERACT_TOOL_SCHEMA,
30
- prepare_for_openai,
31
- prepare_for_groq,
32
- inject_system_hint,
33
27
  extract_message_text,
28
+ inject_system_hint,
34
29
  parse_tool_call_from_text,
30
+ prepare_for_groq,
31
+ prepare_for_openai,
35
32
  synthesize_tool_call_if_missing,
36
33
  )
37
- from .datasets import TaskDatasetSpec, TaskDatasetRegistry
38
34
  from .rubrics import (
39
35
  Criterion,
40
36
  Rubric,
41
- load_rubric,
42
37
  blend_rubrics,
38
+ load_rubric,
43
39
  score_events_against_rubric,
44
40
  score_outcome_against_rubric,
45
41
  )
46
- from .client import TaskAppClient
47
- from .errors import error_payload, http_exception, json_error_response
48
-
49
-
50
42
  from .server import (
51
- TaskAppConfig,
52
43
  ProxyConfig,
53
44
  RubricBundle,
45
+ TaskAppConfig,
54
46
  create_task_app,
55
47
  run_task_app,
56
48
  )
49
+ from .validators import validate_task_app_url
50
+ from .vendors import (
51
+ get_groq_key_or_503,
52
+ get_openai_key_or_503,
53
+ normalize_vendor_keys,
54
+ )
57
55
 
58
56
  __all__ = [
59
57
  "validate_task_app_url",
@@ -1,13 +1,12 @@
1
- from __future__ import annotations
2
-
3
1
  """Registry for Task Apps exposed via the shared FastAPI harness."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import importlib
6
- import os
7
6
  import sys
7
+ from collections.abc import Callable, Iterable, Sequence
8
8
  from dataclasses import dataclass, field
9
9
  from pathlib import Path
10
- from typing import Callable, Dict, Iterable, List, Sequence
11
10
 
12
11
  from ..server import TaskAppConfig
13
12
 
@@ -45,8 +44,8 @@ class TaskAppRegistry:
45
44
  """In-memory registry of known task apps."""
46
45
 
47
46
  def __init__(self) -> None:
48
- self._entries: Dict[str, TaskAppEntry] = {}
49
- self._alias_to_id: Dict[str, str] = {}
47
+ self._entries: dict[str, TaskAppEntry] = {}
48
+ self._alias_to_id: dict[str, str] = {}
50
49
 
51
50
  def register(self, entry: TaskAppEntry) -> None:
52
51
  if entry.app_id in self._entries:
@@ -63,7 +62,7 @@ class TaskAppRegistry:
63
62
  raise KeyError(f"Unknown task app id: {app_id}")
64
63
  return self._entries[resolved]
65
64
 
66
- def list(self) -> List[TaskAppEntry]:
65
+ def list(self) -> list[TaskAppEntry]:
67
66
  return sorted(self._entries.values(), key=lambda entry: entry.app_id)
68
67
 
69
68
  def __iter__(self) -> Iterable[TaskAppEntry]:
@@ -116,7 +115,7 @@ def discover_task_apps_from_cwd() -> None:
116
115
  try:
117
116
  # Import the module to trigger registration
118
117
  importlib.import_module(module_name)
119
- except Exception as exc:
118
+ except Exception:
120
119
  # Silently skip modules that can't be imported
121
120
  # This allows for graceful handling of missing dependencies
122
121
  continue
synth_ai/task/auth.py CHANGED
@@ -1,9 +1,11 @@
1
- from __future__ import annotations
2
-
3
1
  """Authentication helpers shared by Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
- from typing import Iterable, Optional, Any, Set
6
+ from collections.abc import Iterable
7
+ from contextlib import suppress
8
+ from typing import Any
7
9
 
8
10
  from .errors import http_exception
9
11
 
@@ -24,7 +26,7 @@ def _mask(value: str, *, prefix: int = 4) -> str:
24
26
  return f"{visible}{'…' if len(value) > prefix else ''}"
25
27
 
26
28
 
27
- def normalize_environment_api_key() -> Optional[str]:
29
+ def normalize_environment_api_key() -> str | None:
28
30
  """Ensure `ENVIRONMENT_API_KEY` is populated from dev fallbacks.
29
31
 
30
32
  Returns the resolved key (if any) so callers can branch on configuration.
@@ -45,7 +47,7 @@ def normalize_environment_api_key() -> Optional[str]:
45
47
  return None
46
48
 
47
49
 
48
- def allowed_environment_api_keys() -> Set[str]:
50
+ def allowed_environment_api_keys() -> set[str]:
49
51
  """Return the set of valid environment API keys for this Task App.
50
52
 
51
53
  Includes:
@@ -135,7 +137,7 @@ def require_api_key_dependency(request: Any) -> None:
135
137
  bearer.append(a.split(" ", 1)[1].strip())
136
138
  candidates = _split_csv(single + multi + bearer)
137
139
  if not any(candidate in allowed for candidate in candidates):
138
- try:
140
+ with suppress(Exception):
139
141
  print(
140
142
  {
141
143
  "task_auth_failed": True,
@@ -149,8 +151,6 @@ def require_api_key_dependency(request: Any) -> None:
149
151
  },
150
152
  flush=True,
151
153
  )
152
- except Exception:
153
- pass
154
154
  # Use 400 to make failures unmistakable during preflight
155
155
  raise http_exception(
156
156
  400,
synth_ai/task/client.py CHANGED
@@ -1,10 +1,10 @@
1
- from __future__ import annotations
2
-
3
1
  """Async HTTP client for interacting with Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import asyncio
6
- from typing import Any, Dict, Iterable, List, Optional
7
6
  import os
7
+ from typing import Any
8
8
 
9
9
  import httpx
10
10
  from pydantic import BaseModel
@@ -37,7 +37,7 @@ class TaskAppClient:
37
37
  self._client: httpx.AsyncClient | None = None
38
38
  self.env = _TaskAppEnvironmentClient(self)
39
39
 
40
- async def __aenter__(self) -> "TaskAppClient":
40
+ async def __aenter__(self) -> TaskAppClient:
41
41
  await self._ensure_client()
42
42
  return self
43
43
 
@@ -53,8 +53,8 @@ class TaskAppClient:
53
53
  )
54
54
  return self._client
55
55
 
56
- def _headers(self) -> Dict[str, str]:
57
- headers: Dict[str, str] = {}
56
+ def _headers(self) -> dict[str, str]:
57
+ headers: dict[str, str] = {}
58
58
  # Primary key
59
59
  primary = (self.api_key or "").strip()
60
60
  if primary:
@@ -85,7 +85,7 @@ class TaskAppClient:
85
85
  method: str,
86
86
  path: str,
87
87
  *,
88
- params: Optional[Dict[str, Any] | List[tuple[str, Any]]] = None,
88
+ params: dict[str, Any] | list[tuple[str, Any]] | None = None,
89
89
  json_payload: Any = None,
90
90
  ) -> httpx.Response:
91
91
  client = await self._ensure_client()
@@ -118,16 +118,16 @@ class TaskAppClient:
118
118
  raise last_exc
119
119
  raise RuntimeError("Unreachable code in TaskAppClient._request")
120
120
 
121
- async def health(self) -> Dict[str, Any]:
121
+ async def health(self) -> dict[str, Any]:
122
122
  response = await self._request("GET", "/health")
123
123
  return response.json()
124
124
 
125
- async def info(self) -> Dict[str, Any]:
125
+ async def info(self) -> dict[str, Any]:
126
126
  response = await self._request("GET", "/info")
127
127
  return response.json()
128
128
 
129
129
  async def task_info(self, seeds: list[int] | None = None) -> TaskInfo | list[TaskInfo]:
130
- params: Optional[List[tuple[str, Any]]] = None
130
+ params: list[tuple[str, Any]] | None = None
131
131
  if seeds:
132
132
  params = [("seed", seed) for seed in seeds]
133
133
  response = await self._request("GET", "/task_info", params=params)
@@ -146,21 +146,21 @@ class _TaskAppEnvironmentClient:
146
146
  def __init__(self, client: TaskAppClient) -> None:
147
147
  self._client = client
148
148
 
149
- async def initialize(self, env_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
149
+ async def initialize(self, env_name: str, payload: dict[str, Any]) -> dict[str, Any]:
150
150
  response = await self._client._request(
151
151
  "POST", f"/env/{env_name}/initialize", json_payload=payload
152
152
  )
153
153
  return response.json()
154
154
 
155
- async def step(self, env_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
155
+ async def step(self, env_name: str, payload: dict[str, Any]) -> dict[str, Any]:
156
156
  response = await self._client._request(
157
157
  "POST", f"/env/{env_name}/step", json_payload=payload
158
158
  )
159
159
  return response.json()
160
160
 
161
161
  async def terminate(
162
- self, env_name: str, payload: Dict[str, Any] | None = None
163
- ) -> Dict[str, Any]:
162
+ self, env_name: str, payload: dict[str, Any] | None = None
163
+ ) -> dict[str, Any]:
164
164
  response = await self._client._request(
165
165
  "POST", f"/env/{env_name}/terminate", json_payload=payload or {}
166
166
  )
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Optional, Any, Dict, List, Literal
4
+ from typing import Any, Literal
5
+
5
6
  from pydantic import BaseModel, Field
6
7
 
7
8
 
@@ -40,7 +41,7 @@ class TaskAppContract:
40
41
  """
41
42
 
42
43
  base_url: str
43
- env_name: Optional[str] = None
44
+ env_name: str | None = None
44
45
  requires_api_key_header: bool = True
45
46
 
46
47
 
@@ -48,16 +49,16 @@ class TaskAppContract:
48
49
 
49
50
 
50
51
  class RolloutEnvSpec(BaseModel):
51
- env_id: Optional[str] = None
52
- env_name: Optional[str] = None
53
- config: Dict[str, Any] = {}
54
- seed: Optional[int] = None
52
+ env_id: str | None = None
53
+ env_name: str | None = None
54
+ config: dict[str, Any] = Field(default_factory=dict)
55
+ seed: int | None = None
55
56
 
56
57
 
57
58
  class RolloutPolicySpec(BaseModel):
58
- policy_id: Optional[str] = None
59
- policy_name: Optional[str] = None
60
- config: Dict[str, Any] = {}
59
+ policy_id: str | None = None
60
+ policy_name: str | None = None
61
+ config: dict[str, Any] = Field(default_factory=dict)
61
62
 
62
63
 
63
64
  class RolloutRecordConfig(BaseModel):
@@ -77,60 +78,60 @@ class RolloutRequest(BaseModel):
77
78
  run_id: str
78
79
  env: RolloutEnvSpec
79
80
  policy: RolloutPolicySpec
80
- ops: List[Dict[str, Any]] | List[str]
81
+ ops: list[dict[str, Any]] | list[str]
81
82
  record: RolloutRecordConfig = RolloutRecordConfig()
82
83
  on_done: str = "reset"
83
84
  safety: RolloutSafetyConfig = RolloutSafetyConfig()
84
- training_session_id: Optional[str] = None
85
- synth_base_url: Optional[str] = None
85
+ training_session_id: str | None = None
86
+ synth_base_url: str | None = None
86
87
 
87
88
 
88
89
  class RolloutStep(BaseModel):
89
- obs: Dict[str, Any]
90
- tool_calls: List[Dict[str, Any]]
91
- reward: Optional[float] = None
90
+ obs: dict[str, Any]
91
+ tool_calls: list[dict[str, Any]]
92
+ reward: float | None = None
92
93
  done: bool = False
93
- truncated: Optional[bool] = None
94
- info: Optional[Dict[str, Any]] = None
94
+ truncated: bool | None = None
95
+ info: dict[str, Any] | None = None
95
96
 
96
97
 
97
98
  class RolloutTrajectory(BaseModel):
98
99
  env_id: str
99
100
  policy_id: str
100
- steps: List[RolloutStep]
101
- final: Optional[Dict[str, Any]] = None
101
+ steps: list[RolloutStep]
102
+ final: dict[str, Any] | None = None
102
103
  length: int
103
104
 
104
105
 
105
106
  class RolloutMetrics(BaseModel):
106
- episode_returns: List[float]
107
+ episode_returns: list[float]
107
108
  mean_return: float
108
109
  num_steps: int
109
110
  num_episodes: int = 0
110
- outcome_score: Optional[float] = None
111
- events_score: Optional[float] = None
112
- details: Dict[str, Any] = Field(default_factory=dict)
111
+ outcome_score: float | None = None
112
+ events_score: float | None = None
113
+ details: dict[str, Any] = Field(default_factory=dict)
113
114
 
114
115
 
115
116
  class RolloutResponse(BaseModel):
116
117
  run_id: str
117
- trajectories: List[RolloutTrajectory]
118
- branches: Dict[str, List[str]] = {}
118
+ trajectories: list[RolloutTrajectory]
119
+ branches: dict[str, list[str]] = Field(default_factory=dict)
119
120
  metrics: RolloutMetrics
120
121
  aborted: bool = False
121
122
  ops_executed: int = 0
122
- trace: Dict[str, Any] | None = None
123
+ trace: dict[str, Any] | None = None
123
124
 
124
125
 
125
126
  class TaskInfo(BaseModel):
126
127
  """Static metadata describing the capabilities of a Task App task."""
127
128
 
128
- task: Dict[str, Any]
129
- environments: List[str]
130
- action_space: Dict[str, Any]
131
- observation: Dict[str, Any]
132
- dataset: Dict[str, Any]
133
- rubric: Dict[str, Any]
134
- inference: Dict[str, Any]
135
- capabilities: Dict[str, Any]
136
- limits: Dict[str, Any]
129
+ task: dict[str, Any]
130
+ environments: list[str]
131
+ action_space: dict[str, Any]
132
+ observation: dict[str, Any]
133
+ dataset: dict[str, Any]
134
+ rubric: dict[str, Any]
135
+ inference: dict[str, Any]
136
+ capabilities: dict[str, Any]
137
+ limits: dict[str, Any]
synth_ai/task/datasets.py CHANGED
@@ -1,8 +1,9 @@
1
- from __future__ import annotations
2
-
3
1
  """Dataset registry and helpers shared by Task Apps."""
4
2
 
5
- from typing import Any, Callable, Dict, Hashable, Tuple
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Hashable
6
+ from typing import Any
6
7
 
7
8
  from pydantic import BaseModel, Field, field_validator
8
9
 
@@ -34,8 +35,8 @@ class TaskDatasetRegistry:
34
35
  """Lightweight registry mapping dataset specs to loader callables."""
35
36
 
36
37
  def __init__(self) -> None:
37
- self._entries: Dict[str, Tuple[TaskDatasetSpec, RegistryLoader, bool]] = {}
38
- self._cache: Dict[Hashable, Any] = {}
38
+ self._entries: dict[str, tuple[TaskDatasetSpec, RegistryLoader, bool]] = {}
39
+ self._cache: dict[Hashable, Any] = {}
39
40
 
40
41
  def register(
41
42
  self, spec: TaskDatasetSpec, loader: RegistryLoader, *, cache: bool = True
synth_ai/task/errors.py CHANGED
@@ -1,16 +1,16 @@
1
- from __future__ import annotations
2
-
3
1
  """Error helpers used across Task App implementations."""
4
2
 
5
- from typing import Any, Dict, Optional
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
6
 
7
7
  from .json import to_jsonable
8
8
 
9
9
 
10
10
  def error_payload(
11
- code: str, message: str, *, extra: Optional[Dict[str, Any]] = None
12
- ) -> Dict[str, Any]:
13
- payload: Dict[str, Any] = {"error": {"code": code, "message": message}}
11
+ code: str, message: str, *, extra: dict[str, Any] | None = None
12
+ ) -> dict[str, Any]:
13
+ payload: dict[str, Any] = {"error": {"code": code, "message": message}}
14
14
  if extra:
15
15
  payload["error"].update(extra)
16
16
  return payload
@@ -21,8 +21,8 @@ def http_exception(
21
21
  code: str,
22
22
  message: str,
23
23
  *,
24
- extra: Optional[Dict[str, Any]] = None,
25
- headers: Optional[Dict[str, str]] = None,
24
+ extra: dict[str, Any] | None = None,
25
+ headers: dict[str, str] | None = None,
26
26
  ):
27
27
  try:
28
28
  from fastapi import HTTPException # type: ignore
@@ -38,8 +38,8 @@ def json_error_response(
38
38
  code: str,
39
39
  message: str,
40
40
  *,
41
- extra: Optional[Dict[str, Any]] = None,
42
- headers: Optional[Dict[str, str]] = None,
41
+ extra: dict[str, Any] | None = None,
42
+ headers: dict[str, str] | None = None,
43
43
  ):
44
44
  try:
45
45
  from fastapi.responses import JSONResponse # type: ignore
synth_ai/task/health.py CHANGED
@@ -1,10 +1,13 @@
1
+ """Helpers for probing Task App health endpoints."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
- from typing import Any, Dict
5
+ from typing import Any
6
+
4
7
  import aiohttp
5
8
 
6
9
 
7
- async def task_app_health(task_app_url: str) -> Dict[str, Any]:
10
+ async def task_app_health(task_app_url: str) -> dict[str, Any]:
8
11
  """Probe a Task App base URL for basic reachability.
9
12
 
10
13
  Behavior:
@@ -12,15 +15,20 @@ async def task_app_health(task_app_url: str) -> Dict[str, Any]:
12
15
  - Fallback to GET if HEAD is unsupported
13
16
  - Returns {ok: bool, status?: int, error?: str}
14
17
  """
18
+
19
+ async def _try_request(session: aiohttp.ClientSession, method: str) -> dict[str, Any] | None:
20
+ request = getattr(session, method)
21
+ async with request(task_app_url, allow_redirects=True) as response:
22
+ if 200 <= response.status < 400:
23
+ return {"ok": True, "status": response.status}
24
+ return None
25
+
15
26
  try:
16
27
  async with aiohttp.ClientSession() as session:
17
- async with session.head(task_app_url, allow_redirects=True) as r:
18
- if 200 <= r.status < 400:
19
- return {"ok": True, "status": r.status}
20
- async with aiohttp.ClientSession() as session:
21
- async with session.get(task_app_url, allow_redirects=True) as r2:
22
- if 200 <= r2.status < 400:
23
- return {"ok": True, "status": r2.status}
28
+ for method in ("head", "get"):
29
+ result = await _try_request(session, method)
30
+ if result is not None:
31
+ return result
24
32
  return {"ok": False, "status": None}
25
33
  except Exception as e:
26
34
  return {"ok": False, "error": f"{type(e).__name__}: {e}"}