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.
- examples/analyze_semantic_words.sh +2 -2
- examples/baseline/banking77_baseline.py +204 -0
- examples/baseline/crafter_baseline.py +407 -0
- examples/baseline/pokemon_red_baseline.py +326 -0
- examples/baseline/simple_baseline.py +56 -0
- examples/baseline/warming_up_to_rl_baseline.py +239 -0
- examples/blog_posts/gepa/README.md +355 -0
- examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
- examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
- examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
- examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
- examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
- examples/blog_posts/gepa/gepa_baseline.py +204 -0
- examples/blog_posts/gepa/query_prompts_example.py +97 -0
- examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
- examples/blog_posts/gepa/task_apps.py +105 -0
- examples/blog_posts/gepa/test_gepa_local.sh +67 -0
- examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/pokemon_vl/extract_images.py +239 -0
- examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
- examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
- examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
- examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
- examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
- examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
- examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/verilog_rl_lora.toml +80 -123
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
- examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -3
- examples/qwen_vl/README.md +10 -12
- examples/qwen_vl/SETUP_COMPLETE.md +7 -8
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
- examples/qwen_vl/collect_data_via_cli.md +76 -84
- examples/qwen_vl/collect_vision_traces.py +4 -4
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
- examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
- examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
- examples/qwen_vl/run_vision_comparison.sh +6 -7
- examples/rl/README.md +5 -5
- examples/rl/configs/rl_from_base_qwen.toml +26 -1
- examples/rl/configs/rl_from_base_qwen17.toml +6 -2
- examples/rl/task_app/README.md +1 -2
- examples/rl/task_app/math_single_step.py +2 -2
- examples/run_crafter_demo.sh +2 -2
- examples/sft/README.md +1 -1
- examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
- examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
- examples/swe/task_app/README.md +32 -2
- examples/swe/task_app/grpo_swe_mini.py +4 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
- examples/swe/task_app/hosted/inference/openai_client.py +4 -38
- examples/swe/task_app/hosted/policy_routes.py +17 -0
- examples/swe/task_app/hosted/rollout.py +4 -2
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/banking77/__init__.py +6 -0
- examples/task_apps/banking77/banking77_task_app.py +841 -0
- examples/task_apps/banking77/deploy_wrapper.py +46 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/gepa_benchmarks/__init__.py +7 -0
- examples/task_apps/gepa_benchmarks/common.py +260 -0
- examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
- examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
- examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
- examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +288 -39
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +1 -1
- examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
- synth_ai/api/train/builders.py +99 -4
- synth_ai/api/train/cli.py +516 -26
- synth_ai/api/train/config_finder.py +13 -2
- synth_ai/api/train/configs/__init__.py +23 -2
- synth_ai/api/train/configs/prompt_learning.py +442 -0
- synth_ai/api/train/configs/rl.py +61 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/api/train/task_app.py +1 -1
- synth_ai/api/train/validators.py +277 -0
- synth_ai/auth/credentials.py +119 -0
- synth_ai/baseline/__init__.py +25 -0
- synth_ai/baseline/config.py +209 -0
- synth_ai/baseline/discovery.py +214 -0
- synth_ai/baseline/execution.py +146 -0
- synth_ai/cli/__init__.py +94 -18
- synth_ai/cli/__main__.py +0 -0
- synth_ai/cli/claude.py +70 -0
- synth_ai/cli/codex.py +84 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +637 -0
- synth_ai/cli/commands/baseline/list.py +93 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1112 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +424 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +177 -0
- synth_ai/cli/commands/help/core.py +72 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1436 -0
- synth_ai/cli/commands/status/__init__.py +64 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +200 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/validation.py +386 -0
- synth_ai/cli/demo.py +30 -158
- synth_ai/cli/deploy/__init__.py +43 -0
- synth_ai/cli/deploy.py +162 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/legacy_root_backup.py +14 -8
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/opencode.py +107 -0
- synth_ai/cli/root.py +9 -5
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +20 -265
- synth_ai/cli/status.py +7 -126
- synth_ai/cli/task_app_deploy.py +1 -10
- synth_ai/cli/task_app_modal_serve.py +4 -9
- synth_ai/cli/task_app_serve.py +4 -11
- synth_ai/cli/task_apps.py +51 -1480
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +1 -14
- synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/http.py +12 -0
- synth_ai/judge_schemas.py +10 -10
- synth_ai/learning/__init__.py +10 -0
- synth_ai/learning/prompt_learning_client.py +276 -0
- synth_ai/learning/prompt_learning_types.py +184 -0
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/pricing/__init__.py +2 -0
- synth_ai/pricing/model_pricing.py +57 -0
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +518 -0
- synth_ai/streaming/streamer.py +320 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/apps/__init__.py +1 -0
- synth_ai/task/config.py +2 -0
- synth_ai/task/tracing_utils.py +25 -25
- synth_ai/task/validators.py +45 -9
- synth_ai/task_app_cfgs.py +21 -0
- synth_ai/tracing_v3/config.py +162 -19
- synth_ai/tracing_v3/constants.py +1 -1
- synth_ai/tracing_v3/db_config.py +24 -38
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/storage/config.py +47 -13
- synth_ai/tracing_v3/storage/factory.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +113 -11
- synth_ai/tracing_v3/turso/native_manager.py +92 -16
- synth_ai/types.py +8 -0
- synth_ai/urls.py +11 -0
- synth_ai/utils/__init__.py +30 -1
- synth_ai/utils/agents.py +74 -0
- synth_ai/utils/bin.py +39 -0
- synth_ai/utils/cli.py +149 -5
- synth_ai/utils/env.py +40 -33
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/modal.py +285 -3
- synth_ai/utils/paths.py +48 -0
- synth_ai/utils/uvicorn.py +113 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
- examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
- synth_ai/cli/tui.py +0 -62
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -911
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
]
|