synth-ai 0.2.16__py3-none-any.whl → 0.2.17__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/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -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 +42 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -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/train_rl_from_sft.toml +41 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -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 +5 -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 -4
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +66 -3
- 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/inference/openai_client.py +17 -49
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +13 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +15 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- 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 +36 -5
- 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 +2 -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 +134 -3
- 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/inference/openai_client.py +4 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +6 -3
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
- synth_ai/api/train/builders.py +9 -3
- synth_ai/api/train/cli.py +125 -10
- synth_ai/api/train/configs/__init__.py +8 -1
- synth_ai/api/train/configs/rl.py +32 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/auth/credentials.py +119 -0
- synth_ai/cli/__init__.py +12 -4
- synth_ai/cli/commands/__init__.py +17 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/deploy/__init__.py +23 -0
- synth_ai/cli/commands/deploy/core.py +614 -0
- synth_ai/cli/commands/deploy/errors.py +72 -0
- synth_ai/cli/commands/deploy/validation.py +11 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1109 -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 +388 -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 +73 -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/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -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 +199 -0
- synth_ai/cli/commands/train/judge_validation.py +304 -0
- synth_ai/cli/commands/train/validation.py +443 -0
- synth_ai/cli/demo.py +2 -162
- synth_ai/cli/deploy/__init__.py +28 -0
- synth_ai/cli/deploy/core.py +5 -0
- synth_ai/cli/deploy/errors.py +23 -0
- synth_ai/cli/deploy/validation.py +5 -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/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/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 +58 -1487
- 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/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 -11
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +469 -0
- synth_ai/streaming/streamer.py +301 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/validators.py +2 -2
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/utils/env.py +25 -18
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/modal.py +2 -2
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/METADATA +8 -3
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/RECORD +184 -109
- 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.17.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import random
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
from synth_ai.http import AsyncHttpClient, sleep
|
|
9
|
+
|
|
10
|
+
from .config import StreamConfig
|
|
11
|
+
from .handlers import StreamHandler
|
|
12
|
+
from .types import StreamMessage, StreamType
|
|
13
|
+
|
|
14
|
+
TERMINAL_STATUSES = {"succeeded", "failed", "cancelled", "canceled", "completed"}
|
|
15
|
+
TERMINAL_EVENT_SUCCESS = {
|
|
16
|
+
"sft.job.completed",
|
|
17
|
+
"rl.train.completed",
|
|
18
|
+
"rl.job.completed",
|
|
19
|
+
"workflow.completed",
|
|
20
|
+
"training.completed",
|
|
21
|
+
}
|
|
22
|
+
TERMINAL_EVENT_FAILURE = {
|
|
23
|
+
"sft.job.failed",
|
|
24
|
+
"rl.train.failed",
|
|
25
|
+
"rl.job.failed",
|
|
26
|
+
"workflow.failed",
|
|
27
|
+
"training.failed",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class StreamEndpoints:
|
|
33
|
+
"""Collection of endpoint paths (with optional fallbacks) to poll for a job."""
|
|
34
|
+
|
|
35
|
+
status: str | None
|
|
36
|
+
events: str | None = None
|
|
37
|
+
metrics: str | None = None
|
|
38
|
+
timeline: str | None = None
|
|
39
|
+
status_fallbacks: tuple[str, ...] = ()
|
|
40
|
+
event_fallbacks: tuple[str, ...] = ()
|
|
41
|
+
metric_fallbacks: tuple[str, ...] = ()
|
|
42
|
+
timeline_fallbacks: tuple[str, ...] = ()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def learning(cls, job_id: str) -> StreamEndpoints:
|
|
46
|
+
base = f"/learning/jobs/{job_id}"
|
|
47
|
+
return cls(
|
|
48
|
+
status=base,
|
|
49
|
+
events=f"{base}/events",
|
|
50
|
+
metrics=f"{base}/metrics",
|
|
51
|
+
timeline=f"{base}/timeline",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def rl(cls, job_id: str) -> StreamEndpoints:
|
|
56
|
+
base = f"/rl/jobs/{job_id}"
|
|
57
|
+
return cls(
|
|
58
|
+
status=base,
|
|
59
|
+
events=f"{base}/events",
|
|
60
|
+
metrics=f"{base}/metrics",
|
|
61
|
+
timeline=f"{base}/timeline",
|
|
62
|
+
status_fallbacks=(
|
|
63
|
+
f"/learning/jobs/{job_id}",
|
|
64
|
+
f"/orchestration/jobs/{job_id}",
|
|
65
|
+
),
|
|
66
|
+
event_fallbacks=(
|
|
67
|
+
f"/learning/jobs/{job_id}/events",
|
|
68
|
+
f"/orchestration/jobs/{job_id}/events",
|
|
69
|
+
),
|
|
70
|
+
metric_fallbacks=(
|
|
71
|
+
f"/learning/jobs/{job_id}/metrics",
|
|
72
|
+
),
|
|
73
|
+
timeline_fallbacks=(
|
|
74
|
+
f"/learning/jobs/{job_id}/timeline",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class JobStreamer:
|
|
80
|
+
"""Poll job endpoints and dispatch messages to configured handlers."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
base_url: str,
|
|
86
|
+
api_key: str,
|
|
87
|
+
job_id: str,
|
|
88
|
+
endpoints: StreamEndpoints | None = None,
|
|
89
|
+
config: StreamConfig | None = None,
|
|
90
|
+
handlers: Sequence[StreamHandler] | None = None,
|
|
91
|
+
interval_seconds: float = 2.0,
|
|
92
|
+
timeout_seconds: float | None = None,
|
|
93
|
+
http_timeout: float = 60.0,
|
|
94
|
+
http_client: AsyncHttpClient | None = None,
|
|
95
|
+
sleep_fn= sleep,
|
|
96
|
+
) -> None:
|
|
97
|
+
self.base_url = base_url.rstrip("/")
|
|
98
|
+
self.api_key = api_key
|
|
99
|
+
self.job_id = job_id
|
|
100
|
+
self.endpoints = endpoints or StreamEndpoints.learning(job_id)
|
|
101
|
+
self.config = config or StreamConfig.default()
|
|
102
|
+
self.handlers: list[StreamHandler] = list(handlers or [])
|
|
103
|
+
self.interval_seconds = interval_seconds
|
|
104
|
+
self.timeout_seconds = timeout_seconds
|
|
105
|
+
self.http_timeout = http_timeout
|
|
106
|
+
self._http = http_client
|
|
107
|
+
self._sleep = sleep_fn
|
|
108
|
+
|
|
109
|
+
status_sources: list[str | None] = [self.endpoints.status]
|
|
110
|
+
status_sources.extend(self.endpoints.status_fallbacks)
|
|
111
|
+
self._status_paths = [p for p in status_sources if p]
|
|
112
|
+
|
|
113
|
+
event_sources: list[str | None] = [self.endpoints.events]
|
|
114
|
+
event_sources.extend(self.endpoints.event_fallbacks)
|
|
115
|
+
self._event_paths = [p for p in event_sources if p]
|
|
116
|
+
|
|
117
|
+
metric_sources: list[str | None] = [self.endpoints.metrics]
|
|
118
|
+
metric_sources.extend(self.endpoints.metric_fallbacks)
|
|
119
|
+
self._metric_paths = [p for p in metric_sources if p]
|
|
120
|
+
|
|
121
|
+
timeline_sources: list[str | None] = [self.endpoints.timeline]
|
|
122
|
+
timeline_sources.extend(self.endpoints.timeline_fallbacks)
|
|
123
|
+
self._timeline_paths = [p for p in timeline_sources if p]
|
|
124
|
+
|
|
125
|
+
self._last_seq_by_stream: dict[str, int] = {}
|
|
126
|
+
self._last_step_by_metric: dict[str, int] = {}
|
|
127
|
+
self._seen_messages: set[str] = set()
|
|
128
|
+
self._last_status_payload: dict[str, Any] | None = None
|
|
129
|
+
self._last_status_value: str | None = None
|
|
130
|
+
self._terminal_seen = False
|
|
131
|
+
self._terminal_event_status: str | None = None
|
|
132
|
+
|
|
133
|
+
if not self.handlers:
|
|
134
|
+
from .handlers import CLIHandler
|
|
135
|
+
|
|
136
|
+
self.handlers = [CLIHandler()]
|
|
137
|
+
|
|
138
|
+
async def stream_until_terminal(self) -> dict[str, Any]:
|
|
139
|
+
"""Stream configured endpoints until the job reaches a terminal state."""
|
|
140
|
+
http_cm = self._http or AsyncHttpClient(self.base_url, self.api_key, timeout=self.http_timeout)
|
|
141
|
+
async with http_cm as http:
|
|
142
|
+
while True:
|
|
143
|
+
status = await self._refresh_status(http)
|
|
144
|
+
|
|
145
|
+
event_messages = await self._poll_events(http)
|
|
146
|
+
metric_messages = await self._poll_metrics(http)
|
|
147
|
+
timeline_messages = await self._poll_timeline(http)
|
|
148
|
+
|
|
149
|
+
self._dispatch(event_messages + metric_messages + timeline_messages)
|
|
150
|
+
|
|
151
|
+
if self._terminal_seen or (status and status in TERMINAL_STATUSES):
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
await self._sleep(self.interval_seconds)
|
|
155
|
+
|
|
156
|
+
for handler in self.handlers:
|
|
157
|
+
with contextlib.suppress(Exception):
|
|
158
|
+
handler.flush()
|
|
159
|
+
|
|
160
|
+
final_status = self._terminal_event_status or self._last_status_value or "unknown"
|
|
161
|
+
if self._last_status_payload:
|
|
162
|
+
self._last_status_payload["status"] = final_status
|
|
163
|
+
return self._last_status_payload
|
|
164
|
+
return {"job_id": self.job_id, "status": final_status}
|
|
165
|
+
|
|
166
|
+
async def _refresh_status(self, http: AsyncHttpClient) -> str:
|
|
167
|
+
status_payload = await self._poll_status(http)
|
|
168
|
+
if status_payload:
|
|
169
|
+
self._last_status_payload = status_payload
|
|
170
|
+
status = str(status_payload.get("status") or status_payload.get("state") or "").lower()
|
|
171
|
+
if status:
|
|
172
|
+
self._last_status_value = status
|
|
173
|
+
if status in TERMINAL_STATUSES:
|
|
174
|
+
self._terminal_seen = True
|
|
175
|
+
return status
|
|
176
|
+
return self._last_status_value or ""
|
|
177
|
+
|
|
178
|
+
async def _poll_status(self, http: AsyncHttpClient) -> dict[str, Any] | None:
|
|
179
|
+
if StreamType.STATUS not in self.config.enabled_streams or not self._status_paths:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
for path in self._status_paths:
|
|
183
|
+
try:
|
|
184
|
+
data = await http.get(path)
|
|
185
|
+
except Exception:
|
|
186
|
+
continue
|
|
187
|
+
if isinstance(data, dict):
|
|
188
|
+
message = StreamMessage.from_status(self.job_id, data)
|
|
189
|
+
self._dispatch([message])
|
|
190
|
+
return data
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
async def _poll_events(self, http: AsyncHttpClient) -> list[StreamMessage]:
|
|
194
|
+
if StreamType.EVENTS not in self.config.enabled_streams or not self._event_paths:
|
|
195
|
+
return []
|
|
196
|
+
messages: list[StreamMessage] = []
|
|
197
|
+
total = 0
|
|
198
|
+
for path in self._event_paths:
|
|
199
|
+
since = self._last_seq_by_stream.get(path, 0)
|
|
200
|
+
params = {"since_seq": since, "limit": 200}
|
|
201
|
+
try:
|
|
202
|
+
data = await http.get(path, params=params)
|
|
203
|
+
except Exception:
|
|
204
|
+
continue
|
|
205
|
+
raw_events = _extract_list(data, "events")
|
|
206
|
+
for event in raw_events:
|
|
207
|
+
seq = int(event.get("seq") or 0)
|
|
208
|
+
if seq <= self._last_seq_by_stream.get(path, 0):
|
|
209
|
+
continue
|
|
210
|
+
if not self.config.should_include_event(event):
|
|
211
|
+
continue
|
|
212
|
+
self._last_seq_by_stream[path] = seq
|
|
213
|
+
event_job_id = event.get("job_id") or self.job_id
|
|
214
|
+
event_message = StreamMessage.from_event(event_job_id, event)
|
|
215
|
+
event_type = str(event.get("type") or "").lower()
|
|
216
|
+
if event_type in TERMINAL_EVENT_SUCCESS:
|
|
217
|
+
self._terminal_seen = True
|
|
218
|
+
self._terminal_event_status = "succeeded"
|
|
219
|
+
elif event_type in TERMINAL_EVENT_FAILURE:
|
|
220
|
+
self._terminal_seen = True
|
|
221
|
+
self._terminal_event_status = "failed"
|
|
222
|
+
messages.append(event_message)
|
|
223
|
+
total += 1
|
|
224
|
+
if self.config.max_events_per_poll and total >= self.config.max_events_per_poll:
|
|
225
|
+
return messages
|
|
226
|
+
return messages
|
|
227
|
+
|
|
228
|
+
async def _poll_metrics(self, http: AsyncHttpClient) -> list[StreamMessage]:
|
|
229
|
+
if StreamType.METRICS not in self.config.enabled_streams or not self._metric_paths:
|
|
230
|
+
return []
|
|
231
|
+
messages: list[StreamMessage] = []
|
|
232
|
+
for path in self._metric_paths:
|
|
233
|
+
after = max(self._last_step_by_metric.values()) if self._last_step_by_metric else -1
|
|
234
|
+
params = {"after_step": after, "limit": 200}
|
|
235
|
+
try:
|
|
236
|
+
data = await http.get(path, params=params)
|
|
237
|
+
except Exception:
|
|
238
|
+
continue
|
|
239
|
+
points = _extract_list(data, "points")
|
|
240
|
+
for point in points:
|
|
241
|
+
name = point.get("name", "")
|
|
242
|
+
step = int(point.get("step") or -1)
|
|
243
|
+
if step <= self._last_step_by_metric.get(name, -1):
|
|
244
|
+
continue
|
|
245
|
+
if not self.config.should_include_metric(point):
|
|
246
|
+
continue
|
|
247
|
+
self._last_step_by_metric[name] = step
|
|
248
|
+
metric_job_id = point.get("job_id") or self.job_id
|
|
249
|
+
messages.append(StreamMessage.from_metric(metric_job_id, point))
|
|
250
|
+
return messages
|
|
251
|
+
|
|
252
|
+
async def _poll_timeline(self, http: AsyncHttpClient) -> list[StreamMessage]:
|
|
253
|
+
if StreamType.TIMELINE not in self.config.enabled_streams or not self._timeline_paths:
|
|
254
|
+
return []
|
|
255
|
+
messages: list[StreamMessage] = []
|
|
256
|
+
for path in self._timeline_paths:
|
|
257
|
+
try:
|
|
258
|
+
data = await http.get(path)
|
|
259
|
+
except Exception:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
timeline_entries = _extract_list(data, "events")
|
|
263
|
+
for entry in timeline_entries:
|
|
264
|
+
if not self.config.should_include_timeline(entry):
|
|
265
|
+
continue
|
|
266
|
+
timeline_job_id = entry.get("job_id") or self.job_id
|
|
267
|
+
phase = str(entry.get("phase") or "").lower()
|
|
268
|
+
if phase in TERMINAL_STATUSES:
|
|
269
|
+
self._terminal_seen = True
|
|
270
|
+
if phase in {"failed", "cancelled", "canceled"}:
|
|
271
|
+
self._terminal_event_status = "failed"
|
|
272
|
+
elif phase:
|
|
273
|
+
self._terminal_event_status = "succeeded"
|
|
274
|
+
messages.append(StreamMessage.from_timeline(timeline_job_id, entry))
|
|
275
|
+
return messages
|
|
276
|
+
|
|
277
|
+
def _dispatch(self, messages: Iterable[StreamMessage]) -> None:
|
|
278
|
+
for message in messages:
|
|
279
|
+
if self.config.deduplicate and message.key in self._seen_messages:
|
|
280
|
+
continue
|
|
281
|
+
if self.config.sample_rate < 1.0 and random.random() > self.config.sample_rate:
|
|
282
|
+
continue
|
|
283
|
+
if self.config.deduplicate:
|
|
284
|
+
self._seen_messages.add(message.key)
|
|
285
|
+
|
|
286
|
+
for handler in self.handlers:
|
|
287
|
+
try:
|
|
288
|
+
if handler.should_handle(message):
|
|
289
|
+
handler.handle(message)
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _extract_list(data: Any, field: str) -> list[dict[str, Any]]:
|
|
295
|
+
raw = (data or {}).get(field) if isinstance(data, dict) else None
|
|
296
|
+
if isinstance(raw, list):
|
|
297
|
+
return [item for item in raw if isinstance(item, dict)]
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
__all__ = ["JobStreamer", "StreamEndpoints"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StreamType(Enum):
|
|
9
|
+
"""Categories of streaming payloads emitted by training jobs."""
|
|
10
|
+
|
|
11
|
+
STATUS = auto()
|
|
12
|
+
EVENTS = auto()
|
|
13
|
+
METRICS = auto()
|
|
14
|
+
TIMELINE = auto()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def endpoint_path(self) -> str:
|
|
18
|
+
"""Return the endpoint suffix used when polling this stream."""
|
|
19
|
+
return {
|
|
20
|
+
StreamType.STATUS: "",
|
|
21
|
+
StreamType.EVENTS: "/events",
|
|
22
|
+
StreamType.METRICS: "/metrics",
|
|
23
|
+
StreamType.TIMELINE: "/timeline",
|
|
24
|
+
}[self]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class StreamMessage:
|
|
29
|
+
"""Unified representation of a streaming payload."""
|
|
30
|
+
|
|
31
|
+
stream_type: StreamType
|
|
32
|
+
timestamp: str
|
|
33
|
+
job_id: str
|
|
34
|
+
data: dict[str, Any]
|
|
35
|
+
seq: int | None = None
|
|
36
|
+
step: int | None = None
|
|
37
|
+
phase: str | None = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def key(self) -> str:
|
|
41
|
+
"""Return a unique identifier used for deduplication."""
|
|
42
|
+
if self.stream_type is StreamType.EVENTS:
|
|
43
|
+
return f"event:{self.seq}"
|
|
44
|
+
if self.stream_type is StreamType.METRICS:
|
|
45
|
+
name = self.data.get("name", "")
|
|
46
|
+
return f"metric:{name}:{self.step}"
|
|
47
|
+
if self.stream_type is StreamType.TIMELINE:
|
|
48
|
+
return f"timeline:{self.phase}:{self.timestamp}"
|
|
49
|
+
return f"status:{self.timestamp}"
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_status(cls, job_id: str, status_data: dict[str, Any]) -> StreamMessage:
|
|
53
|
+
"""Create a message representing a job status payload."""
|
|
54
|
+
return cls(
|
|
55
|
+
stream_type=StreamType.STATUS,
|
|
56
|
+
timestamp=status_data.get("updated_at", "") or status_data.get("created_at", ""),
|
|
57
|
+
job_id=job_id,
|
|
58
|
+
data=status_data,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_event(cls, job_id: str, event_data: dict[str, Any]) -> StreamMessage:
|
|
63
|
+
"""Create a message describing a job event."""
|
|
64
|
+
return cls(
|
|
65
|
+
stream_type=StreamType.EVENTS,
|
|
66
|
+
timestamp=event_data.get("created_at", ""),
|
|
67
|
+
job_id=job_id,
|
|
68
|
+
data=event_data,
|
|
69
|
+
seq=event_data.get("seq"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_metric(cls, job_id: str, metric_data: dict[str, Any]) -> StreamMessage:
|
|
74
|
+
"""Create a message describing a metric point."""
|
|
75
|
+
return cls(
|
|
76
|
+
stream_type=StreamType.METRICS,
|
|
77
|
+
timestamp=metric_data.get("created_at", ""),
|
|
78
|
+
job_id=job_id,
|
|
79
|
+
data=metric_data,
|
|
80
|
+
step=metric_data.get("step"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_timeline(cls, job_id: str, timeline_data: dict[str, Any]) -> StreamMessage:
|
|
85
|
+
"""Create a message describing a status timeline entry."""
|
|
86
|
+
return cls(
|
|
87
|
+
stream_type=StreamType.TIMELINE,
|
|
88
|
+
timestamp=timeline_data.get("created_at", ""),
|
|
89
|
+
job_id=job_id,
|
|
90
|
+
data=timeline_data,
|
|
91
|
+
phase=timeline_data.get("phase"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = ["StreamMessage", "StreamType"]
|
synth_ai/task/validators.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
from urllib.parse import urlparse, urlunparse
|
|
8
8
|
|
|
9
9
|
import click
|
|
@@ -151,7 +151,7 @@ def normalize_inference_url(url: str | None, *, default: str = "https://api.open
|
|
|
151
151
|
new_path = f"{path}/v1/chat/completions" if path else "/v1/chat/completions"
|
|
152
152
|
|
|
153
153
|
# Reconstruct URL with new path and original query/fragment
|
|
154
|
-
return
|
|
154
|
+
return urlunparse(parsed._replace(path=new_path))
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def validate_task_app_url(url: str | None) -> str:
|
|
@@ -68,7 +68,7 @@ def categorize_files(v2_files: list[tuple[str, list[str]]]) -> dict:
|
|
|
68
68
|
categories["examples"].append((file_path, imports))
|
|
69
69
|
elif any(
|
|
70
70
|
core in file_path
|
|
71
|
-
for core in ["synth_ai/lm/", "synth_ai/
|
|
71
|
+
for core in ["synth_ai/lm/", "synth_ai/environments/"]
|
|
72
72
|
):
|
|
73
73
|
categories["core_library"].append((file_path, imports))
|
|
74
74
|
else:
|
|
@@ -104,7 +104,6 @@ def print_migration_report():
|
|
|
104
104
|
print("2. Debug scripts: Can be deleted or archived")
|
|
105
105
|
print("3. Core library files: Need careful migration to v3")
|
|
106
106
|
print(" - synth_ai/lm/core/main_v2.py")
|
|
107
|
-
print(" - synth_ai/tui/cli/query_experiments.py")
|
|
108
107
|
print(" - synth_ai/environments/service/core_routes.py")
|
|
109
108
|
print("4. Examples: Should be updated to demonstrate v3 usage")
|
|
110
109
|
|
synth_ai/utils/env.py
CHANGED
|
@@ -127,11 +127,17 @@ def filter_json_files_by_key(key: str, paths: list[Path]) -> list[tuple[Path, st
|
|
|
127
127
|
return matches
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def ensure_env_var(key: str, expected_value: str) -> None:
|
|
131
|
+
actual_value = os.getenv(key)
|
|
132
|
+
if expected_value != actual_value:
|
|
133
|
+
raise ValueError(f"Expected: {key}={expected_value}\nActual: {key}={actual_value}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_env_var(key: str) -> str:
|
|
131
137
|
env_value = os.getenv(key)
|
|
132
138
|
if env_value is not None:
|
|
133
139
|
click.echo(f"Using {key}={mask_str(env_value)} from process environment")
|
|
134
|
-
return
|
|
140
|
+
return env_value
|
|
135
141
|
|
|
136
142
|
value: str = ""
|
|
137
143
|
|
|
@@ -167,8 +173,7 @@ def resolve_env_var(key: str) -> None:
|
|
|
167
173
|
show_choices=False,
|
|
168
174
|
).strip()
|
|
169
175
|
except click.Abort:
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
raise
|
|
172
177
|
if choice.lower() == 'm':
|
|
173
178
|
value = _prompt_manual_env_value(key)
|
|
174
179
|
break
|
|
@@ -186,20 +191,22 @@ def resolve_env_var(key: str) -> None:
|
|
|
186
191
|
click.echo(f"Invalid selection. Enter a number between 1 and {len(options)} or 'm'.")
|
|
187
192
|
|
|
188
193
|
else:
|
|
189
|
-
|
|
194
|
+
print(f"No value found for {key}")
|
|
190
195
|
value = _prompt_manual_env_value(key)
|
|
191
196
|
|
|
192
197
|
os.environ[key] = value
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
ensure_env_var(key, value)
|
|
199
|
+
print(f"Loaded {key}={mask_str(value)} into process environment")
|
|
200
|
+
return value
|
|
195
201
|
|
|
196
202
|
|
|
197
203
|
def write_env_var_to_dotenv(
|
|
198
204
|
key: str,
|
|
199
205
|
value: str,
|
|
200
|
-
output_file_path: str | Path,
|
|
206
|
+
output_file_path: str | Path | None = None,
|
|
201
207
|
) -> None:
|
|
202
|
-
path = Path(output_file_path
|
|
208
|
+
path = Path(".env") if output_file_path is None else Path(output_file_path)
|
|
209
|
+
path = path.expanduser()
|
|
203
210
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
211
|
|
|
205
212
|
encoded_value = _format_env_value(value)
|
|
@@ -212,7 +219,7 @@ def write_env_var_to_dotenv(
|
|
|
212
219
|
with path.open('r', encoding="utf-8") as handle:
|
|
213
220
|
lines = handle.readlines()
|
|
214
221
|
except OSError as exc:
|
|
215
|
-
raise
|
|
222
|
+
raise RuntimeError(f"Failed to read {path}: {exc}") from exc
|
|
216
223
|
|
|
217
224
|
for index, line in enumerate(lines):
|
|
218
225
|
parsed = _parse_env_assignment(line)
|
|
@@ -238,9 +245,9 @@ def write_env_var_to_dotenv(
|
|
|
238
245
|
with path.open('w', encoding="utf-8") as handle:
|
|
239
246
|
handle.writelines(lines)
|
|
240
247
|
except OSError as exc:
|
|
241
|
-
raise
|
|
248
|
+
raise RuntimeError(f"Failed to write {path}: {exc}") from exc
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
print(f"Wrote {key}={mask_str(value)} to {path.resolve()}")
|
|
244
251
|
|
|
245
252
|
|
|
246
253
|
def write_env_var_to_json(
|
|
@@ -250,7 +257,7 @@ def write_env_var_to_json(
|
|
|
250
257
|
) -> None:
|
|
251
258
|
path = Path(output_file_path).expanduser()
|
|
252
259
|
if path.exists() and not path.is_file():
|
|
253
|
-
raise
|
|
260
|
+
raise RuntimeError(f"{path} exists and is not a file")
|
|
254
261
|
|
|
255
262
|
data: dict[str, str] = {}
|
|
256
263
|
|
|
@@ -259,12 +266,12 @@ def write_env_var_to_json(
|
|
|
259
266
|
with path.open('r', encoding="utf-8") as handle:
|
|
260
267
|
existing = json.load(handle)
|
|
261
268
|
except json.JSONDecodeError as exc:
|
|
262
|
-
raise
|
|
269
|
+
raise RuntimeError(f"Invalid JSON in {path}: {exc}") from exc
|
|
263
270
|
except OSError as exc:
|
|
264
|
-
raise
|
|
271
|
+
raise RuntimeError(f"Failed to read {path}: {exc}") from exc
|
|
265
272
|
|
|
266
273
|
if not isinstance(existing, dict):
|
|
267
|
-
raise
|
|
274
|
+
raise RuntimeError(f"Expected JSON object in {path}")
|
|
268
275
|
|
|
269
276
|
for existing_key, existing_value in existing.items():
|
|
270
277
|
if existing_key == key:
|
|
@@ -282,6 +289,6 @@ def write_env_var_to_json(
|
|
|
282
289
|
json.dump(data, handle, indent=2, sort_keys=True)
|
|
283
290
|
handle.write('\n')
|
|
284
291
|
except OSError as exc:
|
|
285
|
-
raise
|
|
292
|
+
raise RuntimeError(f"Failed to write {path}: {exc}") from exc
|
|
286
293
|
|
|
287
|
-
|
|
294
|
+
print(f"Wrote {key}={mask_str(value)} to {path}")
|
synth_ai/utils/http.py
CHANGED
|
@@ -34,7 +34,10 @@ class AsyncHttpClient:
|
|
|
34
34
|
|
|
35
35
|
async def __aenter__(self) -> AsyncHttpClient:
|
|
36
36
|
if self._session is None:
|
|
37
|
-
headers = {
|
|
37
|
+
headers = {
|
|
38
|
+
"authorization": f"Bearer {self._api_key}",
|
|
39
|
+
"accept": "application/json",
|
|
40
|
+
}
|
|
38
41
|
user_id = os.getenv("SYNTH_USER_ID") or os.getenv("X_USER_ID") or os.getenv("USER_ID")
|
|
39
42
|
if user_id:
|
|
40
43
|
headers["X-User-ID"] = user_id
|
synth_ai/utils/modal.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import shutil
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
from urllib.parse import urlparse, urlunparse
|
|
9
9
|
|
|
10
10
|
from synth_ai.demos import core as demo_core
|
|
@@ -63,7 +63,7 @@ def normalize_endpoint_url(url: str) -> str:
|
|
|
63
63
|
creds += f":{parsed.password}"
|
|
64
64
|
netloc = f"{creds}@{netloc}"
|
|
65
65
|
parsed = parsed._replace(netloc=netloc)
|
|
66
|
-
return
|
|
66
|
+
return urlunparse(parsed)
|
|
67
67
|
except Exception:
|
|
68
68
|
pass
|
|
69
69
|
return url
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synth-ai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.17
|
|
4
4
|
Summary: RL as a service SDK - Core AI functionality and tracing
|
|
5
5
|
Author-email: Synth AI <josh@usesynth.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,7 @@ Requires-Dist: tqdm>=4.66.4
|
|
|
19
19
|
Requires-Dist: jsonschema>=4.23.0
|
|
20
20
|
Requires-Dist: backoff>=2.0.0
|
|
21
21
|
Requires-Dist: typing_extensions>=4.0.0
|
|
22
|
+
Requires-Dist: rich>=13.9.0
|
|
22
23
|
Requires-Dist: openai>=1.99.0
|
|
23
24
|
Requires-Dist: anthropic>=0.42.0
|
|
24
25
|
Requires-Dist: langfuse<3.0.0,>=2.53.9
|
|
@@ -46,7 +47,6 @@ Requires-Dist: google-api-core>=2.25.1
|
|
|
46
47
|
Requires-Dist: google-generativeai>=0.8.5
|
|
47
48
|
Requires-Dist: crafter>=1.8.3
|
|
48
49
|
Requires-Dist: click<8.2,>=8.1.7
|
|
49
|
-
Requires-Dist: textual>=1.1.0
|
|
50
50
|
Requires-Dist: openai-harmony>=0.0.1
|
|
51
51
|
Requires-Dist: asyncpg>=0.30.0
|
|
52
52
|
Requires-Dist: aiohttp>=3.8.0
|
|
@@ -71,9 +71,14 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
71
71
|
Provides-Extra: research
|
|
72
72
|
Requires-Dist: crafter>=1.8.3; extra == "research"
|
|
73
73
|
Requires-Dist: datasets>=4.0.0; extra == "research"
|
|
74
|
+
Provides-Extra: swe
|
|
75
|
+
Requires-Dist: morphcloud>=0.1.3; extra == "swe"
|
|
76
|
+
Requires-Dist: swebench>=2.3.0; extra == "swe"
|
|
74
77
|
Provides-Extra: all
|
|
75
78
|
Requires-Dist: crafter>=1.8.3; extra == "all"
|
|
76
79
|
Requires-Dist: datasets>=4.0.0; extra == "all"
|
|
80
|
+
Requires-Dist: morphcloud>=0.1.3; extra == "all"
|
|
81
|
+
Requires-Dist: swebench>=2.3.0; extra == "all"
|
|
77
82
|
Provides-Extra: analytics
|
|
78
83
|
Requires-Dist: pandas>=2.2.3; extra == "analytics"
|
|
79
84
|
Dynamic: license-file
|
|
@@ -92,7 +97,7 @@ Dynamic: license-file
|
|
|
92
97
|
|
|
93
98
|
---
|
|
94
99
|
|
|
95
|
-
## 🚀 Install
|
|
100
|
+
## 🚀 Install version 0.2.16
|
|
96
101
|
|
|
97
102
|
```bash
|
|
98
103
|
pip install synth-ai
|