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
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from .types import StreamType
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class StreamConfig:
11
+ """Configuration describing which streams to consume and how to filter them."""
12
+
13
+ enabled_streams: set[StreamType] = field(default_factory=lambda: set(StreamType))
14
+ event_types: set[str] | None = None # Whitelist: only include these event types
15
+ event_types_exclude: set[str] | None = None # Blacklist: exclude these event types
16
+ event_levels: set[str] | None = None
17
+ metric_names: set[str] | None = None
18
+ metric_phases: set[str] | None = None
19
+ timeline_phases: set[str] | None = None
20
+ sample_rate: float = 1.0
21
+ max_events_per_poll: int | None = None
22
+ deduplicate: bool = True
23
+
24
+ @classmethod
25
+ def default(cls) -> StreamConfig:
26
+ """Return a configuration representing the default (all streams) view."""
27
+ return cls(
28
+ event_types_exclude={
29
+ # Filter out noisy events that just announce what metrics already show
30
+ "sft.progress", # Generic "Training progress" with no data
31
+ "sft.loss", # Generic "Loss update" with no data
32
+ "sft.upstream.status", # Very verbose status echo events
33
+ }
34
+ )
35
+
36
+ @classmethod
37
+ def minimal(cls) -> StreamConfig:
38
+ """Return a configuration streaming status updates only."""
39
+ return cls(enabled_streams={StreamType.STATUS})
40
+
41
+ @classmethod
42
+ def verbose(cls) -> StreamConfig:
43
+ """Return a configuration with all streams and events (no filters)."""
44
+ return cls()
45
+
46
+ @classmethod
47
+ def progress_only(cls) -> StreamConfig:
48
+ """Return a configuration tailored to show training progress."""
49
+ return cls(
50
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
51
+ event_types={"sft.progress", "rl.train.step", "sft.validation.summary"},
52
+ metric_names={"train.loss", "eval.reward_mean"},
53
+ )
54
+
55
+ @classmethod
56
+ def errors_only(cls) -> StreamConfig:
57
+ """Return a configuration that focuses on heightened severity signals."""
58
+ return cls(
59
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS},
60
+ event_levels={"error", "warning"},
61
+ )
62
+
63
+ def should_include_event(self, event: dict[str, Any]) -> bool:
64
+ """Determine whether an event message should be included."""
65
+ event_type = event.get("type")
66
+
67
+ # Apply blacklist first (takes precedence)
68
+ if self.event_types_exclude and event_type in self.event_types_exclude:
69
+ return False
70
+
71
+ # Then apply whitelist
72
+ if self.event_types and event_type not in self.event_types:
73
+ return False
74
+
75
+ if self.event_levels:
76
+ return event.get("level") in self.event_levels
77
+ return True
78
+
79
+ def should_include_metric(self, metric: dict[str, Any]) -> bool:
80
+ """Determine whether a metric point should be included."""
81
+ if self.metric_names and metric.get("name") not in self.metric_names:
82
+ return False
83
+ if self.metric_phases:
84
+ return metric.get("phase") in self.metric_phases
85
+ return True
86
+
87
+ def should_include_timeline(self, timeline_entry: dict[str, Any]) -> bool:
88
+ """Determine whether a timeline entry should be included."""
89
+ if self.timeline_phases:
90
+ return timeline_entry.get("phase") in self.timeline_phases
91
+ return True
92
+
93
+
94
+ __all__ = ["StreamConfig"]
@@ -0,0 +1,518 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import json
5
+ import re
6
+ import time
7
+ from abc import ABC, abstractmethod
8
+ from collections import deque
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Callable
12
+
13
+ import click
14
+
15
+ from .types import StreamMessage, StreamType
16
+
17
+
18
+ def _mask_sensitive_urls(text: str) -> str:
19
+ """Mask S3/Wasabi URLs and sensitive paths in log messages.
20
+
21
+ Replaces full S3/Wasabi URLs with masked versions to prevent leaking
22
+ bucket names, paths, and infrastructure details in public SDK logs.
23
+
24
+ Examples:
25
+ s3://synth-artifacts/models/... -> s3://***/***/[masked]
26
+ Wasabi s3://bucket/path/file.tar.gz -> Wasabi s3://***/***/[masked]
27
+ """
28
+ if not text:
29
+ return text
30
+
31
+ # Pattern matches:
32
+ # - Optional "Wasabi " prefix
33
+ # - s3:// or http(s):// scheme
34
+ # - Any bucket/host
35
+ # - Any path
36
+ # - Common model file extensions
37
+ pattern = r'(Wasabi\s+)?((s3|https?)://[^\s]+\.(tar\.gz|zip|pt|pth|safetensors|ckpt|bin))'
38
+
39
+ def replace_url(match: re.Match) -> str:
40
+ prefix = match.group(1) or "" # "Wasabi " or empty
41
+ url = match.group(2)
42
+ # Extract just the filename
43
+ filename = url.split("/")[-1] if "/" in url else "file"
44
+ return f'{prefix}s3://***/***/[{filename}]'
45
+
46
+ return re.sub(pattern, replace_url, text, flags=re.IGNORECASE)
47
+
48
+
49
+ class StreamHandler(ABC):
50
+ """Base class for log handlers that consume ``StreamMessage`` objects."""
51
+
52
+ @abstractmethod
53
+ def handle(self, message: StreamMessage) -> None:
54
+ """Process a message produced by the streamer."""
55
+
56
+ def should_handle(self, message: StreamMessage) -> bool: # pragma: no cover - trivial
57
+ """Predicate allowing handlers to filter messages before processing."""
58
+ return True
59
+
60
+ def flush(self) -> None: # pragma: no cover - optional
61
+ """Flush buffered output."""
62
+ return None
63
+
64
+
65
+ class CLIHandler(StreamHandler):
66
+ """Simple CLI output mirroring current poller behaviour."""
67
+
68
+ def __init__(
69
+ self,
70
+ *,
71
+ hidden_event_types: set[str] | None = None,
72
+ hidden_event_substrings: set[str] | None = None,
73
+ ) -> None:
74
+ self._hidden_event_types = set(hidden_event_types or set())
75
+ self._hidden_event_substrings = {s.lower() for s in (hidden_event_substrings or set())}
76
+
77
+ def handle(self, message: StreamMessage) -> None:
78
+ if not self.should_handle(message):
79
+ return
80
+
81
+ timestamp = datetime.now().strftime("%H:%M:%S")
82
+ if message.stream_type is StreamType.STATUS:
83
+ status = str(message.data.get("status") or message.data.get("state") or "unknown")
84
+ click.echo(f"[{timestamp}] status={status}")
85
+ return
86
+
87
+ if message.stream_type is StreamType.EVENTS:
88
+ event_type = message.data.get("type", "event")
89
+ if event_type in self._hidden_event_types:
90
+ return
91
+ level = message.data.get("level")
92
+ msg = message.data.get("message") or ""
93
+ # Evaluate substring filters against lower-cased concatenated text
94
+ if self._hidden_event_substrings:
95
+ blob = " ".join(
96
+ [
97
+ event_type or "",
98
+ str(msg),
99
+ json.dumps(message.data.get("data", "")),
100
+ ]
101
+ ).lower()
102
+ if any(sub in blob for sub in self._hidden_event_substrings):
103
+ return
104
+ prefix = f"[{timestamp}] [{message.seq}] {event_type}"
105
+ if level:
106
+ prefix += f" ({level})"
107
+ # Mask sensitive URLs before displaying
108
+ sanitized_msg = _mask_sensitive_urls(msg)
109
+ click.echo(f"{prefix}: {sanitized_msg}".rstrip(": "))
110
+ return
111
+
112
+ if message.stream_type is StreamType.METRICS:
113
+ name = message.data.get("name")
114
+ value = message.data.get("value")
115
+ step = message.data.get("step")
116
+ data = message.data.get("data", {})
117
+
118
+ # Format metric display
119
+ metric_str = f"[{timestamp}] [metric] {name}={value:.4f}" if isinstance(value, (int, float)) else f"[{timestamp}] [metric] {name}={value}"
120
+ if step is not None:
121
+ metric_str += f" (step={step})"
122
+
123
+ # Add any additional context from data field
124
+ if isinstance(data, dict):
125
+ n = data.get("n")
126
+ if n is not None:
127
+ metric_str += f" n={n}"
128
+
129
+ click.echo(metric_str)
130
+ return
131
+
132
+ if message.stream_type is StreamType.TIMELINE:
133
+ phase = message.data.get("phase", "phase")
134
+ click.echo(f"[{timestamp}] timeline={phase}")
135
+
136
+
137
+ class JSONHandler(StreamHandler):
138
+ """Emit messages as JSON lines suitable for machine parsing."""
139
+
140
+ def __init__(self, output_file: str | None = None, *, indent: int | None = None) -> None:
141
+ self.output_file = Path(output_file).expanduser() if output_file else None
142
+ self._indent = indent
143
+
144
+ def handle(self, message: StreamMessage) -> None:
145
+ if not self.should_handle(message):
146
+ return
147
+
148
+ payload: dict[str, Any] = {
149
+ "stream_type": message.stream_type.name,
150
+ "timestamp": message.timestamp,
151
+ "job_id": message.job_id,
152
+ "data": message.data,
153
+ }
154
+ if message.seq is not None:
155
+ payload["seq"] = message.seq
156
+ if message.step is not None:
157
+ payload["step"] = message.step
158
+ if message.phase is not None:
159
+ payload["phase"] = message.phase
160
+
161
+ line = json.dumps(payload, indent=self._indent)
162
+ if self.output_file:
163
+ with self.output_file.open("a", encoding="utf-8") as fh:
164
+ fh.write(line)
165
+ if self._indent is None:
166
+ fh.write("\n")
167
+ else:
168
+ click.echo(line)
169
+
170
+ def flush(self) -> None:
171
+ return None
172
+
173
+
174
+ class CallbackHandler(StreamHandler):
175
+ """Invoke user-provided callbacks for specific stream types."""
176
+
177
+ def __init__(
178
+ self,
179
+ *,
180
+ on_status: Callable[[dict[str, Any]], None] | None = None,
181
+ on_event: Callable[[dict[str, Any]], None] | None = None,
182
+ on_metric: Callable[[dict[str, Any]], None] | None = None,
183
+ on_timeline: Callable[[dict[str, Any]], None] | None = None,
184
+ ) -> None:
185
+ self._on_status = on_status
186
+ self._on_event = on_event
187
+ self._on_metric = on_metric
188
+ self._on_timeline = on_timeline
189
+
190
+ def handle(self, message: StreamMessage) -> None:
191
+ if not self.should_handle(message):
192
+ return
193
+
194
+ if message.stream_type is StreamType.STATUS and self._on_status:
195
+ self._on_status(message.data)
196
+ elif message.stream_type is StreamType.EVENTS and self._on_event:
197
+ self._on_event(message.data)
198
+ elif message.stream_type is StreamType.METRICS and self._on_metric:
199
+ self._on_metric(message.data)
200
+ elif message.stream_type is StreamType.TIMELINE and self._on_timeline:
201
+ self._on_timeline(message.data)
202
+
203
+
204
+ class BufferedHandler(StreamHandler):
205
+ """Collect messages and emit them in batches."""
206
+
207
+ def __init__(self, *, flush_interval: float = 5.0, max_buffer_size: int = 100) -> None:
208
+ self.flush_interval = flush_interval
209
+ self.max_buffer_size = max_buffer_size
210
+ self._buffer: list[StreamMessage] = []
211
+ self._last_flush = time.time()
212
+
213
+ def handle(self, message: StreamMessage) -> None:
214
+ if not self.should_handle(message):
215
+ return
216
+
217
+ self._buffer.append(message)
218
+ now = time.time()
219
+ if len(self._buffer) >= self.max_buffer_size or now - self._last_flush >= self.flush_interval:
220
+ self.flush()
221
+
222
+ def flush(self) -> None:
223
+ if not self._buffer:
224
+ return
225
+ self.process_batch(self._buffer)
226
+ self._buffer.clear()
227
+ self._last_flush = time.time()
228
+
229
+ def process_batch(self, messages: list[StreamMessage]) -> None: # pragma: no cover - abstract
230
+ """Override to define how buffered messages should be processed."""
231
+
232
+
233
+ class IntegrationTestHandler(StreamHandler):
234
+ """Collect messages for integration tests or programmatic assertions."""
235
+
236
+ def __init__(self) -> None:
237
+ self.messages: list[StreamMessage] = []
238
+
239
+ def handle(self, message: StreamMessage) -> None:
240
+ self.messages.append(message)
241
+
242
+ def clear(self) -> None:
243
+ self.messages.clear()
244
+
245
+
246
+ class LossCurveHandler(StreamHandler):
247
+ """Render a live-updating loss chart inside a fixed Rich panel."""
248
+
249
+ def __init__(
250
+ self,
251
+ *,
252
+ metric_name: str = "train.loss",
253
+ max_points: int = 200,
254
+ width: int = 60,
255
+ console: Any | None = None,
256
+ live: Any | None = None,
257
+ ) -> None:
258
+ try:
259
+ from rich.console import Console
260
+ from rich.live import Live
261
+ from rich.panel import Panel
262
+ from rich.text import Text
263
+ except ImportError as exc: # pragma: no cover - optional dependency guard
264
+ raise RuntimeError(
265
+ "LossCurveHandler requires the 'rich' package. Install synth-ai[analytics] or rich>=13."
266
+ ) from exc
267
+
268
+ self.metric_name = metric_name
269
+ self.max_points = max_points
270
+ self.width = width
271
+
272
+ self._console_class = Console
273
+ self._panel_class = Panel
274
+ self._text_class = Text
275
+
276
+ self._console = console or Console()
277
+ self._live = live or Live(console=self._console, transient=False, refresh_per_second=8)
278
+ self._started = False
279
+
280
+ self._steps: list[int] = []
281
+ self._values: list[float] = []
282
+ self._status = "waiting"
283
+ self._last_event: str | None = None
284
+
285
+ def handle(self, message: StreamMessage) -> None:
286
+ updated = False
287
+
288
+ if message.stream_type is StreamType.STATUS:
289
+ status = str(message.data.get("status") or message.data.get("state") or "unknown")
290
+ if status != self._status:
291
+ self._status = status
292
+ updated = True
293
+
294
+ elif message.stream_type is StreamType.EVENTS:
295
+ event_type = message.data.get("type", "")
296
+ msg = message.data.get("message") or ""
297
+ level = message.data.get("level")
298
+ summary = f"{event_type}".strip()
299
+ if level:
300
+ summary += f" ({level})"
301
+ if msg:
302
+ summary += f": {msg}"
303
+ if summary != self._last_event:
304
+ self._last_event = summary
305
+ updated = True
306
+
307
+ elif message.stream_type is StreamType.METRICS:
308
+ if message.data.get("name") != self.metric_name:
309
+ return
310
+ value = message.data.get("value")
311
+ step = message.data.get("step")
312
+ if not isinstance(value, (int, float)) or not isinstance(step, int):
313
+ return
314
+ self._values.append(float(value))
315
+ self._steps.append(step)
316
+ if len(self._values) > self.max_points:
317
+ self._values = self._values[-self.max_points :]
318
+ self._steps = self._steps[-self.max_points :]
319
+ updated = True
320
+
321
+ elif message.stream_type is StreamType.TIMELINE:
322
+ phase = message.data.get("phase")
323
+ if phase:
324
+ self._status = str(phase)
325
+ updated = True
326
+
327
+ if updated:
328
+ self._refresh()
329
+
330
+ def flush(self) -> None:
331
+ if self._started:
332
+ with contextlib.suppress(Exception):
333
+ self._live.stop()
334
+ self._started = False
335
+
336
+ def _ensure_live(self) -> None:
337
+ if not self._started:
338
+ with contextlib.suppress(Exception):
339
+ self._live.start()
340
+ self._started = True
341
+
342
+ def _refresh(self) -> None:
343
+ self._ensure_live()
344
+ body = self._build_body()
345
+ title = f"{self.metric_name} | status={self._status}"
346
+ self._live.update(self._panel_class(body, title=title, border_style="cyan"))
347
+
348
+ def _build_body(self) -> Any:
349
+ if not self._values:
350
+ return self._text_class("Waiting for metrics…", style="yellow")
351
+
352
+ chart = self._render_sparkline()
353
+ last_value = self._values[-1]
354
+ lines = [
355
+ chart,
356
+ f"latest: {last_value:.4f} (step {self._steps[-1]})",
357
+ ]
358
+ if self._last_event:
359
+ lines.append(f"event: {self._last_event}")
360
+ return "\n".join(lines)
361
+
362
+ def _render_sparkline(self) -> str:
363
+ blocks = "▁▂▃▄▅▆▇█"
364
+ tail_len = min(self.width, len(self._values))
365
+ tail = self._values[-tail_len:]
366
+ minimum = min(tail)
367
+ maximum = max(tail)
368
+ if maximum == minimum:
369
+ level = blocks[0]
370
+ return f"{minimum:.2f} {level * tail_len} {maximum:.2f}"
371
+ scale = (len(blocks) - 1) / (maximum - minimum)
372
+ chars = "".join(blocks[int((v - minimum) * scale + 0.5)] for v in tail)
373
+ return f"{minimum:.2f} {chars} {maximum:.2f}"
374
+
375
+ def __del__(self) -> None: # pragma: no cover - defensive cleanup
376
+ with contextlib.suppress(Exception):
377
+ self.flush()
378
+
379
+ class RichHandler(StreamHandler):
380
+ """Rich powered handler with live progress and metrics table."""
381
+
382
+ def __init__(
383
+ self,
384
+ *,
385
+ event_log_size: int = 20,
386
+ console: Any | None = None,
387
+ ) -> None:
388
+ try:
389
+ from rich.console import Console
390
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
391
+ from rich.table import Table
392
+ except ImportError as exc: # pragma: no cover - requires optional dependency
393
+ raise RuntimeError(
394
+ "RichHandler requires the 'rich' package. Install synth-ai[analytics] or rich>=13."
395
+ ) from exc
396
+
397
+ self._console_class = Console
398
+ self._progress_class = Progress
399
+ self._spinner_column = SpinnerColumn
400
+ self._text_column = TextColumn
401
+ self._bar_column = BarColumn
402
+ self._table_class = Table
403
+
404
+ self._console = console or Console()
405
+ self._progress = Progress(
406
+ SpinnerColumn(),
407
+ TextColumn("[progress.description]{task.description}"),
408
+ BarColumn(),
409
+ TextColumn("{task.completed}/{task.total}" if console else ""),
410
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
411
+ transient=False,
412
+ console=self._console,
413
+ )
414
+ self._task_id: int | None = None
415
+ self._current_status = "unknown"
416
+ self._latest_metrics: dict[str, Any] = {}
417
+ self._event_log: deque[str] = deque(maxlen=event_log_size)
418
+ self._progress_started = False
419
+
420
+ def handle(self, message: StreamMessage) -> None:
421
+ if not self.should_handle(message):
422
+ return
423
+
424
+ if message.stream_type is StreamType.STATUS:
425
+ self._current_status = str(message.data.get("status") or message.data.get("state"))
426
+ self._ensure_progress_started()
427
+ if self._task_id is not None:
428
+ description = f"Status: {self._current_status}"
429
+ self._progress.update(self._task_id, description=description)
430
+ self._render_summary()
431
+ return
432
+
433
+ if message.stream_type is StreamType.EVENTS:
434
+ event_type = message.data.get("type", "event")
435
+ summary = message.data.get("message") or ""
436
+ level = message.data.get("level")
437
+ # Mask sensitive URLs before displaying
438
+ sanitized_summary = _mask_sensitive_urls(summary)
439
+ formatted = f"[{event_type}] {sanitized_summary}".strip()
440
+ if level:
441
+ formatted = f"{formatted} ({level})"
442
+ self._event_log.append(formatted)
443
+ data = message.data.get("data") or {}
444
+ step = data.get("step") or data.get("current_step")
445
+ total_steps = data.get("total_steps") or data.get("max_steps")
446
+ if step and total_steps:
447
+ self._ensure_progress_started(total_steps)
448
+ if self._task_id is not None:
449
+ self._progress.update(self._task_id, completed=int(step), total=int(total_steps))
450
+ self._render_summary()
451
+ return
452
+
453
+ if message.stream_type is StreamType.METRICS:
454
+ name = message.data.get("name", "")
455
+ value = message.data.get("value")
456
+ if name:
457
+ self._latest_metrics[name] = value
458
+ self._render_summary()
459
+ return
460
+
461
+ if message.stream_type is StreamType.TIMELINE:
462
+ phase = message.data.get("phase", "")
463
+ if phase and phase.lower() not in {"training", "running"}:
464
+ self._event_log.append(f"[timeline] {phase}")
465
+ self._render_summary()
466
+
467
+ def flush(self) -> None:
468
+ if self._progress_started:
469
+ self._progress.stop()
470
+ self._progress_started = False
471
+ self._render_summary(force=True)
472
+
473
+ def _ensure_progress_started(self, total: int | float | None = None) -> None:
474
+ if not self._progress_started:
475
+ self._progress.start()
476
+ self._progress_started = True
477
+ if self._task_id is None:
478
+ self._task_id = self._progress.add_task(
479
+ f"Status: {self._current_status}", total=total or 100
480
+ )
481
+ elif total is not None and self._task_id is not None:
482
+ self._progress.update(self._task_id, total=total)
483
+
484
+ def _render_summary(self, force: bool = False) -> None:
485
+ if force and self._progress_started:
486
+ self._progress.refresh()
487
+
488
+ table = self._table_class(title="Latest Metrics")
489
+ table.add_column("Metric")
490
+ table.add_column("Value")
491
+
492
+ if not self._latest_metrics:
493
+ table.add_row("—", "—")
494
+ else:
495
+ for name, value in sorted(self._latest_metrics.items()):
496
+ table.add_row(str(name), str(value))
497
+
498
+ if self._progress_started:
499
+ self._progress.console.print(table)
500
+ else:
501
+ self._console.print(table)
502
+
503
+ if self._event_log:
504
+ self._console.print("\nRecent events:")
505
+ for entry in list(self._event_log):
506
+ self._console.print(f" • {entry}")
507
+
508
+
509
+ __all__ = [
510
+ "BufferedHandler",
511
+ "CallbackHandler",
512
+ "CLIHandler",
513
+ "JSONHandler",
514
+ "IntegrationTestHandler",
515
+ "LossCurveHandler",
516
+ "RichHandler",
517
+ "StreamHandler",
518
+ ]