synth-ai 0.4.1__py3-none-any.whl → 0.4.4__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.
- synth_ai/__init__.py +13 -13
- synth_ai/cli/__init__.py +6 -15
- synth_ai/cli/commands/eval/__init__.py +6 -15
- synth_ai/cli/commands/eval/config.py +338 -0
- synth_ai/cli/commands/eval/core.py +236 -1091
- synth_ai/cli/commands/eval/runner.py +704 -0
- synth_ai/cli/commands/eval/validation.py +44 -117
- synth_ai/cli/commands/filter/core.py +7 -7
- synth_ai/cli/commands/filter/validation.py +2 -2
- synth_ai/cli/commands/smoke/core.py +7 -17
- synth_ai/cli/commands/status/__init__.py +1 -64
- synth_ai/cli/commands/status/client.py +50 -151
- synth_ai/cli/commands/status/config.py +3 -83
- synth_ai/cli/commands/status/errors.py +4 -13
- synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
- synth_ai/cli/commands/status/subcommands/config.py +13 -0
- synth_ai/cli/commands/status/subcommands/files.py +18 -63
- synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
- synth_ai/cli/commands/status/subcommands/models.py +18 -62
- synth_ai/cli/commands/status/subcommands/runs.py +16 -63
- synth_ai/cli/commands/status/subcommands/session.py +67 -172
- synth_ai/cli/commands/status/subcommands/summary.py +24 -32
- synth_ai/cli/commands/status/subcommands/utils.py +41 -0
- synth_ai/cli/commands/status/utils.py +16 -107
- synth_ai/cli/commands/train/__init__.py +18 -20
- synth_ai/cli/commands/train/errors.py +3 -3
- synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
- synth_ai/cli/commands/train/validation.py +7 -7
- synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
- synth_ai/cli/commands/train/verifier_validation.py +235 -0
- synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
- synth_ai/cli/lib/apps/task_app.py +12 -13
- synth_ai/cli/lib/task_app_discovery.py +6 -6
- synth_ai/cli/lib/train_cfgs.py +10 -10
- synth_ai/cli/task_apps/__init__.py +11 -0
- synth_ai/cli/task_apps/commands.py +7 -15
- synth_ai/core/env.py +12 -1
- synth_ai/core/errors.py +1 -2
- synth_ai/core/integrations/cloudflare.py +209 -33
- synth_ai/core/tracing_v3/abstractions.py +46 -0
- synth_ai/data/__init__.py +3 -30
- synth_ai/data/enums.py +1 -20
- synth_ai/data/rewards.py +100 -3
- synth_ai/products/graph_evolve/__init__.py +1 -2
- synth_ai/products/graph_evolve/config.py +16 -16
- synth_ai/products/graph_evolve/converters/__init__.py +3 -3
- synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
- synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
- synth_ai/products/graph_gepa/__init__.py +23 -0
- synth_ai/products/graph_gepa/converters/__init__.py +19 -0
- synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
- synth_ai/sdk/__init__.py +45 -35
- synth_ai/sdk/api/eval/__init__.py +33 -0
- synth_ai/sdk/api/eval/job.py +732 -0
- synth_ai/sdk/api/research_agent/__init__.py +276 -66
- synth_ai/sdk/api/train/builders.py +181 -0
- synth_ai/sdk/api/train/cli.py +41 -33
- synth_ai/sdk/api/train/configs/__init__.py +6 -4
- synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
- synth_ai/sdk/api/train/configs/rl.py +264 -16
- synth_ai/sdk/api/train/configs/sft.py +165 -1
- synth_ai/sdk/api/train/graph_validators.py +12 -12
- synth_ai/sdk/api/train/graphgen.py +169 -51
- synth_ai/sdk/api/train/graphgen_models.py +95 -45
- synth_ai/sdk/api/train/local_api.py +10 -0
- synth_ai/sdk/api/train/pollers.py +36 -0
- synth_ai/sdk/api/train/prompt_learning.py +390 -60
- synth_ai/sdk/api/train/rl.py +41 -5
- synth_ai/sdk/api/train/sft.py +2 -0
- synth_ai/sdk/api/train/task_app.py +20 -0
- synth_ai/sdk/api/train/validators.py +17 -17
- synth_ai/sdk/graphs/completions.py +239 -33
- synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
- synth_ai/sdk/learning/__init__.py +35 -5
- synth_ai/sdk/learning/context_learning_client.py +531 -0
- synth_ai/sdk/learning/context_learning_types.py +294 -0
- synth_ai/sdk/learning/prompt_learning_client.py +1 -1
- synth_ai/sdk/learning/prompt_learning_types.py +2 -1
- synth_ai/sdk/learning/rl/__init__.py +0 -4
- synth_ai/sdk/learning/rl/contracts.py +0 -4
- synth_ai/sdk/localapi/__init__.py +40 -0
- synth_ai/sdk/localapi/apps/__init__.py +28 -0
- synth_ai/sdk/localapi/client.py +10 -0
- synth_ai/sdk/localapi/contracts.py +10 -0
- synth_ai/sdk/localapi/helpers.py +519 -0
- synth_ai/sdk/localapi/rollouts.py +93 -0
- synth_ai/sdk/localapi/server.py +29 -0
- synth_ai/sdk/localapi/template.py +49 -0
- synth_ai/sdk/streaming/handlers.py +6 -6
- synth_ai/sdk/streaming/streamer.py +10 -6
- synth_ai/sdk/task/__init__.py +18 -5
- synth_ai/sdk/task/apps/__init__.py +37 -1
- synth_ai/sdk/task/client.py +9 -1
- synth_ai/sdk/task/config.py +6 -11
- synth_ai/sdk/task/contracts.py +137 -95
- synth_ai/sdk/task/in_process.py +32 -22
- synth_ai/sdk/task/in_process_runner.py +9 -4
- synth_ai/sdk/task/rubrics/__init__.py +2 -3
- synth_ai/sdk/task/rubrics/loaders.py +4 -4
- synth_ai/sdk/task/rubrics/strict.py +3 -4
- synth_ai/sdk/task/server.py +76 -16
- synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
- synth_ai/sdk/task/validators.py +34 -49
- synth_ai/sdk/training/__init__.py +7 -16
- synth_ai/sdk/tunnels/__init__.py +118 -0
- synth_ai/sdk/tunnels/cleanup.py +83 -0
- synth_ai/sdk/tunnels/ports.py +120 -0
- synth_ai/sdk/tunnels/tunneled_api.py +363 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
- synth_ai/cli/commands/baseline/__init__.py +0 -12
- synth_ai/cli/commands/baseline/core.py +0 -636
- synth_ai/cli/commands/baseline/list.py +0 -94
- synth_ai/cli/commands/eval/errors.py +0 -81
- synth_ai/cli/commands/status/formatters.py +0 -164
- synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
- synth_ai/cli/commands/status/subcommands/usage.py +0 -203
- synth_ai/cli/commands/train/judge_validation.py +0 -305
- synth_ai/cli/usage.py +0 -159
- synth_ai/data/specs.py +0 -36
- synth_ai/sdk/api/research_agent/cli.py +0 -428
- synth_ai/sdk/api/research_agent/config.py +0 -357
- synth_ai/sdk/api/research_agent/job.py +0 -717
- synth_ai/sdk/baseline/__init__.py +0 -25
- synth_ai/sdk/baseline/config.py +0 -209
- synth_ai/sdk/baseline/discovery.py +0 -216
- synth_ai/sdk/baseline/execution.py +0 -154
- synth_ai/sdk/judging/__init__.py +0 -15
- synth_ai/sdk/judging/base.py +0 -24
- synth_ai/sdk/judging/client.py +0 -191
- synth_ai/sdk/judging/types.py +0 -42
- synth_ai/sdk/research_agent/__init__.py +0 -34
- synth_ai/sdk/research_agent/container_builder.py +0 -328
- synth_ai/sdk/research_agent/container_spec.py +0 -198
- synth_ai/sdk/research_agent/defaults.py +0 -34
- synth_ai/sdk/research_agent/results_collector.py +0 -69
- synth_ai/sdk/specs/__init__.py +0 -46
- synth_ai/sdk/specs/dataclasses.py +0 -149
- synth_ai/sdk/specs/loader.py +0 -144
- synth_ai/sdk/specs/serializer.py +0 -199
- synth_ai/sdk/specs/validation.py +0 -250
- synth_ai/sdk/tracing/__init__.py +0 -39
- synth_ai/sdk/usage/__init__.py +0 -37
- synth_ai/sdk/usage/client.py +0 -171
- synth_ai/sdk/usage/models.py +0 -261
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Client for interacting with context learning jobs.
|
|
2
|
+
|
|
3
|
+
Context Learning optimizes environment setup scripts (pre-flight/post-flight bash)
|
|
4
|
+
for terminal/coding agents. This is "prompt optimization" for terminal agents.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
from synth_ai.sdk.learning import ContextLearningClient, ContextLearningJobConfig
|
|
8
|
+
|
|
9
|
+
client = ContextLearningClient("http://localhost:8000", "your-api-key")
|
|
10
|
+
|
|
11
|
+
# Create a job from config
|
|
12
|
+
config = ContextLearningJobConfig.from_dict({
|
|
13
|
+
"task_app_url": "http://localhost:8102",
|
|
14
|
+
"evaluation_seeds": [0, 1, 2, 3, 4],
|
|
15
|
+
"environment": {
|
|
16
|
+
"preflight_script": "#!/bin/bash\\necho 'Setting up...'",
|
|
17
|
+
},
|
|
18
|
+
"algorithm_config": {
|
|
19
|
+
"initial_population_size": 10,
|
|
20
|
+
"num_generations": 3,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
job_id = await client.create_job(config)
|
|
25
|
+
|
|
26
|
+
# Stream events
|
|
27
|
+
async for event in client.stream_events(job_id):
|
|
28
|
+
print(f"{event.event_type}: {event.message}")
|
|
29
|
+
|
|
30
|
+
# Get best script when done
|
|
31
|
+
result = await client.get_best_script(job_id)
|
|
32
|
+
print(result.preflight_script)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
39
|
+
|
|
40
|
+
from synth_ai.core._utils.http import AsyncHttpClient
|
|
41
|
+
|
|
42
|
+
from .context_learning_types import (
|
|
43
|
+
BestScriptResult,
|
|
44
|
+
ContextLearningEvent,
|
|
45
|
+
ContextLearningJobConfig,
|
|
46
|
+
ContextLearningJobStatus,
|
|
47
|
+
ContextLearningMetric,
|
|
48
|
+
ContextLearningResults,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_job_id(job_id: str) -> None:
|
|
53
|
+
"""Validate that job_id has the expected context learning format.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
job_id: Job ID to validate
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If job_id doesn't start with 'cl_job_'
|
|
60
|
+
"""
|
|
61
|
+
if not job_id.startswith("cl_job_"):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Invalid context learning job ID format: {job_id!r}. "
|
|
64
|
+
f"Expected format: 'cl_job_<identifier>' (e.g., 'cl_job_abc123def456')"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ContextLearningClient:
|
|
69
|
+
"""Async client for interacting with context learning jobs.
|
|
70
|
+
|
|
71
|
+
This client provides a Pythonic interface for:
|
|
72
|
+
- Creating and managing context learning jobs
|
|
73
|
+
- Streaming events and metrics
|
|
74
|
+
- Retrieving best scripts and results
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> client = ContextLearningClient("http://localhost:8000", "your-api-key")
|
|
78
|
+
>>> job_id = await client.create_job(config)
|
|
79
|
+
>>> status = await client.get_job(job_id)
|
|
80
|
+
>>> print(status.status) # "running"
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = 60.0) -> None:
|
|
84
|
+
"""Initialize the context learning client.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
base_url: Base URL of the backend API (e.g., "http://localhost:8000")
|
|
88
|
+
api_key: API key for authentication
|
|
89
|
+
timeout: Request timeout in seconds (default: 60s for long operations)
|
|
90
|
+
"""
|
|
91
|
+
self._base_url = base_url.rstrip("/")
|
|
92
|
+
self._api_key = api_key
|
|
93
|
+
self._timeout = timeout
|
|
94
|
+
|
|
95
|
+
# -------------------------------------------------------------------------
|
|
96
|
+
# Job Management
|
|
97
|
+
# -------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
async def create_job(
|
|
100
|
+
self,
|
|
101
|
+
config: ContextLearningJobConfig | Dict[str, Any],
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Create a new context learning job.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Job configuration (ContextLearningJobConfig or dict)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Job ID (e.g., "cl_job_abc123def456")
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
RuntimeError: If job creation fails
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(config, dict):
|
|
115
|
+
config = ContextLearningJobConfig.from_dict(config)
|
|
116
|
+
|
|
117
|
+
payload = config.to_dict()
|
|
118
|
+
|
|
119
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
120
|
+
result = await http.post_json("/api/context-learning/jobs", json=payload)
|
|
121
|
+
|
|
122
|
+
job_id = result.get("job_id")
|
|
123
|
+
if not job_id:
|
|
124
|
+
raise RuntimeError(f"Job creation failed: response missing job_id. Response: {result}")
|
|
125
|
+
|
|
126
|
+
return job_id
|
|
127
|
+
|
|
128
|
+
async def get_job(self, job_id: str) -> ContextLearningJobStatus:
|
|
129
|
+
"""Get job details and status.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
job_id: Job ID
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Job status with details
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If job_id format is invalid
|
|
139
|
+
"""
|
|
140
|
+
_validate_job_id(job_id)
|
|
141
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
142
|
+
result = await http.get(f"/api/context-learning/jobs/{job_id}")
|
|
143
|
+
return ContextLearningJobStatus.from_dict(result)
|
|
144
|
+
|
|
145
|
+
async def list_jobs(self, *, limit: int = 50) -> List[ContextLearningJobStatus]:
|
|
146
|
+
"""List context learning jobs.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
limit: Maximum number of jobs to return
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of job statuses, sorted by created_at descending
|
|
153
|
+
"""
|
|
154
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
155
|
+
result = await http.get("/api/context-learning/jobs", params={"limit": limit})
|
|
156
|
+
|
|
157
|
+
if isinstance(result, list):
|
|
158
|
+
return [ContextLearningJobStatus.from_dict(j) for j in result]
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
async def start_job(self, job_id: str) -> ContextLearningJobStatus:
|
|
162
|
+
"""Start a pending job.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
job_id: Job ID
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Updated job status
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If job_id format is invalid
|
|
172
|
+
RuntimeError: If job cannot be started
|
|
173
|
+
"""
|
|
174
|
+
_validate_job_id(job_id)
|
|
175
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
176
|
+
result = await http.post_json(f"/api/context-learning/jobs/{job_id}/start", json={})
|
|
177
|
+
return ContextLearningJobStatus.from_dict(result)
|
|
178
|
+
|
|
179
|
+
async def cancel_job(self, job_id: str) -> ContextLearningJobStatus:
|
|
180
|
+
"""Cancel a running job.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
job_id: Job ID
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Updated job status
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If job_id format is invalid
|
|
190
|
+
RuntimeError: If job cannot be cancelled
|
|
191
|
+
"""
|
|
192
|
+
_validate_job_id(job_id)
|
|
193
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
194
|
+
result = await http.post_json(f"/api/context-learning/jobs/{job_id}/cancel", json={})
|
|
195
|
+
return ContextLearningJobStatus.from_dict(result)
|
|
196
|
+
|
|
197
|
+
# -------------------------------------------------------------------------
|
|
198
|
+
# Events and Metrics
|
|
199
|
+
# -------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
async def get_events(
|
|
202
|
+
self, job_id: str, *, limit: int = 100
|
|
203
|
+
) -> List[ContextLearningEvent]:
|
|
204
|
+
"""Get events for a job.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
job_id: Job ID
|
|
208
|
+
limit: Maximum number of events to return
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of events, most recent first
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If job_id format is invalid
|
|
215
|
+
"""
|
|
216
|
+
_validate_job_id(job_id)
|
|
217
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
218
|
+
result = await http.get(
|
|
219
|
+
f"/api/context-learning/jobs/{job_id}/events",
|
|
220
|
+
params={"limit": limit},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if isinstance(result, list):
|
|
224
|
+
return [ContextLearningEvent.from_dict(e) for e in result]
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
async def get_metrics(
|
|
228
|
+
self, job_id: str, *, name: Optional[str] = None, limit: int = 500
|
|
229
|
+
) -> List[ContextLearningMetric]:
|
|
230
|
+
"""Get metrics for a job.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
job_id: Job ID
|
|
234
|
+
name: Optional metric name filter
|
|
235
|
+
limit: Maximum number of metrics to return
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of metrics
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If job_id format is invalid
|
|
242
|
+
"""
|
|
243
|
+
_validate_job_id(job_id)
|
|
244
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
245
|
+
if name:
|
|
246
|
+
params["name"] = name
|
|
247
|
+
|
|
248
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
249
|
+
result = await http.get(
|
|
250
|
+
f"/api/context-learning/jobs/{job_id}/metrics",
|
|
251
|
+
params=params,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
points = result.get("points", []) if isinstance(result, dict) else []
|
|
255
|
+
return [ContextLearningMetric.from_dict(p) for p in points]
|
|
256
|
+
|
|
257
|
+
async def stream_events(
|
|
258
|
+
self,
|
|
259
|
+
job_id: str,
|
|
260
|
+
*,
|
|
261
|
+
poll_interval: float = 2.0,
|
|
262
|
+
timeout: Optional[float] = None,
|
|
263
|
+
) -> AsyncIterator[ContextLearningEvent]:
|
|
264
|
+
"""Stream events from a job until it reaches a terminal state.
|
|
265
|
+
|
|
266
|
+
This is a polling-based implementation that yields new events as they arrive.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
job_id: Job ID
|
|
270
|
+
poll_interval: Seconds between polls
|
|
271
|
+
timeout: Maximum seconds to stream (None = no timeout)
|
|
272
|
+
|
|
273
|
+
Yields:
|
|
274
|
+
Events as they occur
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError: If job_id format is invalid
|
|
278
|
+
asyncio.TimeoutError: If timeout is reached
|
|
279
|
+
"""
|
|
280
|
+
_validate_job_id(job_id)
|
|
281
|
+
|
|
282
|
+
seen_seqs: set[int] = set()
|
|
283
|
+
start_time = asyncio.get_event_loop().time()
|
|
284
|
+
|
|
285
|
+
while True:
|
|
286
|
+
# Check timeout
|
|
287
|
+
if timeout is not None:
|
|
288
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
289
|
+
if elapsed >= timeout:
|
|
290
|
+
raise asyncio.TimeoutError(f"Stream timeout after {timeout}s")
|
|
291
|
+
|
|
292
|
+
# Get job status
|
|
293
|
+
status = await self.get_job(job_id)
|
|
294
|
+
|
|
295
|
+
# Get events
|
|
296
|
+
events = await self.get_events(job_id, limit=500)
|
|
297
|
+
|
|
298
|
+
# Yield new events
|
|
299
|
+
for event in events:
|
|
300
|
+
seq = event.seq
|
|
301
|
+
if seq is not None and seq not in seen_seqs:
|
|
302
|
+
seen_seqs.add(seq)
|
|
303
|
+
yield event
|
|
304
|
+
|
|
305
|
+
# Check if done
|
|
306
|
+
if status.is_terminal:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
# Wait before next poll
|
|
310
|
+
await asyncio.sleep(poll_interval)
|
|
311
|
+
|
|
312
|
+
# -------------------------------------------------------------------------
|
|
313
|
+
# Results
|
|
314
|
+
# -------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
async def get_best_script(self, job_id: str) -> BestScriptResult:
|
|
317
|
+
"""Get the best performing pre-flight script.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
job_id: Job ID
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Best script result with score and metadata
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
ValueError: If job_id format is invalid
|
|
327
|
+
RuntimeError: If job is not completed or no best script available
|
|
328
|
+
"""
|
|
329
|
+
_validate_job_id(job_id)
|
|
330
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
331
|
+
result = await http.get(f"/api/context-learning/jobs/{job_id}/best-script")
|
|
332
|
+
return BestScriptResult.from_dict(result)
|
|
333
|
+
|
|
334
|
+
async def get_results(self, job_id: str) -> ContextLearningResults:
|
|
335
|
+
"""Get complete results from a job.
|
|
336
|
+
|
|
337
|
+
Aggregates status, events, and best script into a single result object.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
job_id: Job ID
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Complete results including status, events, and best script
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
ValueError: If job_id format is invalid
|
|
347
|
+
"""
|
|
348
|
+
_validate_job_id(job_id)
|
|
349
|
+
|
|
350
|
+
status = await self.get_job(job_id)
|
|
351
|
+
events = await self.get_events(job_id, limit=1000)
|
|
352
|
+
|
|
353
|
+
best_script: Optional[BestScriptResult] = None
|
|
354
|
+
if status.is_successful:
|
|
355
|
+
try:
|
|
356
|
+
best_script = await self.get_best_script(job_id)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
return ContextLearningResults.from_status_and_events(status, events, best_script)
|
|
361
|
+
|
|
362
|
+
# -------------------------------------------------------------------------
|
|
363
|
+
# Convenience Methods
|
|
364
|
+
# -------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
async def wait_for_completion(
|
|
367
|
+
self,
|
|
368
|
+
job_id: str,
|
|
369
|
+
*,
|
|
370
|
+
poll_interval: float = 5.0,
|
|
371
|
+
timeout: Optional[float] = None,
|
|
372
|
+
on_event: Optional[callable] = None,
|
|
373
|
+
) -> ContextLearningJobStatus:
|
|
374
|
+
"""Wait for a job to complete.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
job_id: Job ID
|
|
378
|
+
poll_interval: Seconds between status checks
|
|
379
|
+
timeout: Maximum seconds to wait (None = no timeout)
|
|
380
|
+
on_event: Optional callback for each new event
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Final job status
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
ValueError: If job_id format is invalid
|
|
387
|
+
asyncio.TimeoutError: If timeout is reached
|
|
388
|
+
"""
|
|
389
|
+
async for event in self.stream_events(job_id, poll_interval=poll_interval, timeout=timeout):
|
|
390
|
+
if on_event:
|
|
391
|
+
try:
|
|
392
|
+
on_event(event)
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
return await self.get_job(job_id)
|
|
397
|
+
|
|
398
|
+
async def run_job(
|
|
399
|
+
self,
|
|
400
|
+
config: ContextLearningJobConfig | Dict[str, Any],
|
|
401
|
+
*,
|
|
402
|
+
poll_interval: float = 5.0,
|
|
403
|
+
timeout: Optional[float] = None,
|
|
404
|
+
on_event: Optional[callable] = None,
|
|
405
|
+
) -> ContextLearningResults:
|
|
406
|
+
"""Create a job, wait for completion, and return results.
|
|
407
|
+
|
|
408
|
+
This is the main entry point for running context learning jobs.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
config: Job configuration
|
|
412
|
+
poll_interval: Seconds between status checks
|
|
413
|
+
timeout: Maximum seconds to wait (None = no timeout)
|
|
414
|
+
on_event: Optional callback for each new event
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Complete results including best script
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
>>> config = ContextLearningJobConfig.from_dict({
|
|
421
|
+
... "task_app_url": "http://localhost:8102",
|
|
422
|
+
... "evaluation_seeds": [0, 1, 2],
|
|
423
|
+
... })
|
|
424
|
+
>>> results = await client.run_job(config)
|
|
425
|
+
>>> print(results.best_script.preflight_script)
|
|
426
|
+
"""
|
|
427
|
+
job_id = await self.create_job(config)
|
|
428
|
+
|
|
429
|
+
await self.wait_for_completion(
|
|
430
|
+
job_id,
|
|
431
|
+
poll_interval=poll_interval,
|
|
432
|
+
timeout=timeout,
|
|
433
|
+
on_event=on_event,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return await self.get_results(job_id)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# -----------------------------------------------------------------------------
|
|
440
|
+
# Synchronous Wrappers
|
|
441
|
+
# -----------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
def create_job(
|
|
444
|
+
config: ContextLearningJobConfig | Dict[str, Any],
|
|
445
|
+
base_url: str,
|
|
446
|
+
api_key: str,
|
|
447
|
+
) -> str:
|
|
448
|
+
"""Synchronous wrapper to create a context learning job.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
config: Job configuration
|
|
452
|
+
base_url: Backend API base URL
|
|
453
|
+
api_key: API key for authentication
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Job ID
|
|
457
|
+
"""
|
|
458
|
+
client = ContextLearningClient(base_url, api_key)
|
|
459
|
+
return asyncio.run(client.create_job(config))
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def get_job_status(
|
|
463
|
+
job_id: str,
|
|
464
|
+
base_url: str,
|
|
465
|
+
api_key: str,
|
|
466
|
+
) -> ContextLearningJobStatus:
|
|
467
|
+
"""Synchronous wrapper to get job status.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
job_id: Job ID
|
|
471
|
+
base_url: Backend API base URL
|
|
472
|
+
api_key: API key for authentication
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Job status
|
|
476
|
+
"""
|
|
477
|
+
client = ContextLearningClient(base_url, api_key)
|
|
478
|
+
return asyncio.run(client.get_job(job_id))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def get_best_script(
|
|
482
|
+
job_id: str,
|
|
483
|
+
base_url: str,
|
|
484
|
+
api_key: str,
|
|
485
|
+
) -> BestScriptResult:
|
|
486
|
+
"""Synchronous wrapper to get best script.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
job_id: Job ID
|
|
490
|
+
base_url: Backend API base URL
|
|
491
|
+
api_key: API key for authentication
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Best script result
|
|
495
|
+
"""
|
|
496
|
+
client = ContextLearningClient(base_url, api_key)
|
|
497
|
+
return asyncio.run(client.get_best_script(job_id))
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def run_job(
|
|
501
|
+
config: ContextLearningJobConfig | Dict[str, Any],
|
|
502
|
+
base_url: str,
|
|
503
|
+
api_key: str,
|
|
504
|
+
*,
|
|
505
|
+
poll_interval: float = 5.0,
|
|
506
|
+
timeout: Optional[float] = None,
|
|
507
|
+
on_event: Optional[callable] = None,
|
|
508
|
+
) -> ContextLearningResults:
|
|
509
|
+
"""Synchronous wrapper to run a complete job.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
config: Job configuration
|
|
513
|
+
base_url: Backend API base URL
|
|
514
|
+
api_key: API key for authentication
|
|
515
|
+
poll_interval: Seconds between status checks
|
|
516
|
+
timeout: Maximum seconds to wait
|
|
517
|
+
on_event: Optional callback for each event
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Complete results
|
|
521
|
+
"""
|
|
522
|
+
client = ContextLearningClient(base_url, api_key)
|
|
523
|
+
return asyncio.run(
|
|
524
|
+
client.run_job(
|
|
525
|
+
config,
|
|
526
|
+
poll_interval=poll_interval,
|
|
527
|
+
timeout=timeout,
|
|
528
|
+
on_event=on_event,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|