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.

Files changed (153) hide show
  1. synth_ai/__init__.py +13 -13
  2. synth_ai/cli/__init__.py +6 -15
  3. synth_ai/cli/commands/eval/__init__.py +6 -15
  4. synth_ai/cli/commands/eval/config.py +338 -0
  5. synth_ai/cli/commands/eval/core.py +236 -1091
  6. synth_ai/cli/commands/eval/runner.py +704 -0
  7. synth_ai/cli/commands/eval/validation.py +44 -117
  8. synth_ai/cli/commands/filter/core.py +7 -7
  9. synth_ai/cli/commands/filter/validation.py +2 -2
  10. synth_ai/cli/commands/smoke/core.py +7 -17
  11. synth_ai/cli/commands/status/__init__.py +1 -64
  12. synth_ai/cli/commands/status/client.py +50 -151
  13. synth_ai/cli/commands/status/config.py +3 -83
  14. synth_ai/cli/commands/status/errors.py +4 -13
  15. synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
  16. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  17. synth_ai/cli/commands/status/subcommands/files.py +18 -63
  18. synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
  19. synth_ai/cli/commands/status/subcommands/models.py +18 -62
  20. synth_ai/cli/commands/status/subcommands/runs.py +16 -63
  21. synth_ai/cli/commands/status/subcommands/session.py +67 -172
  22. synth_ai/cli/commands/status/subcommands/summary.py +24 -32
  23. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  24. synth_ai/cli/commands/status/utils.py +16 -107
  25. synth_ai/cli/commands/train/__init__.py +18 -20
  26. synth_ai/cli/commands/train/errors.py +3 -3
  27. synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
  28. synth_ai/cli/commands/train/validation.py +7 -7
  29. synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
  30. synth_ai/cli/commands/train/verifier_validation.py +235 -0
  31. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
  32. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
  33. synth_ai/cli/demo_apps/math/config.toml +0 -1
  34. synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
  35. synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
  36. synth_ai/cli/lib/apps/task_app.py +12 -13
  37. synth_ai/cli/lib/task_app_discovery.py +6 -6
  38. synth_ai/cli/lib/train_cfgs.py +10 -10
  39. synth_ai/cli/task_apps/__init__.py +11 -0
  40. synth_ai/cli/task_apps/commands.py +7 -15
  41. synth_ai/core/env.py +12 -1
  42. synth_ai/core/errors.py +1 -2
  43. synth_ai/core/integrations/cloudflare.py +209 -33
  44. synth_ai/core/tracing_v3/abstractions.py +46 -0
  45. synth_ai/data/__init__.py +3 -30
  46. synth_ai/data/enums.py +1 -20
  47. synth_ai/data/rewards.py +100 -3
  48. synth_ai/products/graph_evolve/__init__.py +1 -2
  49. synth_ai/products/graph_evolve/config.py +16 -16
  50. synth_ai/products/graph_evolve/converters/__init__.py +3 -3
  51. synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
  52. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
  53. synth_ai/products/graph_gepa/__init__.py +23 -0
  54. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  55. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  56. synth_ai/sdk/__init__.py +45 -35
  57. synth_ai/sdk/api/eval/__init__.py +33 -0
  58. synth_ai/sdk/api/eval/job.py +732 -0
  59. synth_ai/sdk/api/research_agent/__init__.py +276 -66
  60. synth_ai/sdk/api/train/builders.py +181 -0
  61. synth_ai/sdk/api/train/cli.py +41 -33
  62. synth_ai/sdk/api/train/configs/__init__.py +6 -4
  63. synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
  64. synth_ai/sdk/api/train/configs/rl.py +264 -16
  65. synth_ai/sdk/api/train/configs/sft.py +165 -1
  66. synth_ai/sdk/api/train/graph_validators.py +12 -12
  67. synth_ai/sdk/api/train/graphgen.py +169 -51
  68. synth_ai/sdk/api/train/graphgen_models.py +95 -45
  69. synth_ai/sdk/api/train/local_api.py +10 -0
  70. synth_ai/sdk/api/train/pollers.py +36 -0
  71. synth_ai/sdk/api/train/prompt_learning.py +390 -60
  72. synth_ai/sdk/api/train/rl.py +41 -5
  73. synth_ai/sdk/api/train/sft.py +2 -0
  74. synth_ai/sdk/api/train/task_app.py +20 -0
  75. synth_ai/sdk/api/train/validators.py +17 -17
  76. synth_ai/sdk/graphs/completions.py +239 -33
  77. synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
  78. synth_ai/sdk/learning/__init__.py +35 -5
  79. synth_ai/sdk/learning/context_learning_client.py +531 -0
  80. synth_ai/sdk/learning/context_learning_types.py +294 -0
  81. synth_ai/sdk/learning/prompt_learning_client.py +1 -1
  82. synth_ai/sdk/learning/prompt_learning_types.py +2 -1
  83. synth_ai/sdk/learning/rl/__init__.py +0 -4
  84. synth_ai/sdk/learning/rl/contracts.py +0 -4
  85. synth_ai/sdk/localapi/__init__.py +40 -0
  86. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  87. synth_ai/sdk/localapi/client.py +10 -0
  88. synth_ai/sdk/localapi/contracts.py +10 -0
  89. synth_ai/sdk/localapi/helpers.py +519 -0
  90. synth_ai/sdk/localapi/rollouts.py +93 -0
  91. synth_ai/sdk/localapi/server.py +29 -0
  92. synth_ai/sdk/localapi/template.py +49 -0
  93. synth_ai/sdk/streaming/handlers.py +6 -6
  94. synth_ai/sdk/streaming/streamer.py +10 -6
  95. synth_ai/sdk/task/__init__.py +18 -5
  96. synth_ai/sdk/task/apps/__init__.py +37 -1
  97. synth_ai/sdk/task/client.py +9 -1
  98. synth_ai/sdk/task/config.py +6 -11
  99. synth_ai/sdk/task/contracts.py +137 -95
  100. synth_ai/sdk/task/in_process.py +32 -22
  101. synth_ai/sdk/task/in_process_runner.py +9 -4
  102. synth_ai/sdk/task/rubrics/__init__.py +2 -3
  103. synth_ai/sdk/task/rubrics/loaders.py +4 -4
  104. synth_ai/sdk/task/rubrics/strict.py +3 -4
  105. synth_ai/sdk/task/server.py +76 -16
  106. synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
  107. synth_ai/sdk/task/validators.py +34 -49
  108. synth_ai/sdk/training/__init__.py +7 -16
  109. synth_ai/sdk/tunnels/__init__.py +118 -0
  110. synth_ai/sdk/tunnels/cleanup.py +83 -0
  111. synth_ai/sdk/tunnels/ports.py +120 -0
  112. synth_ai/sdk/tunnels/tunneled_api.py +363 -0
  113. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
  114. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
  115. synth_ai/cli/commands/baseline/__init__.py +0 -12
  116. synth_ai/cli/commands/baseline/core.py +0 -636
  117. synth_ai/cli/commands/baseline/list.py +0 -94
  118. synth_ai/cli/commands/eval/errors.py +0 -81
  119. synth_ai/cli/commands/status/formatters.py +0 -164
  120. synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
  121. synth_ai/cli/commands/status/subcommands/usage.py +0 -203
  122. synth_ai/cli/commands/train/judge_validation.py +0 -305
  123. synth_ai/cli/usage.py +0 -159
  124. synth_ai/data/specs.py +0 -36
  125. synth_ai/sdk/api/research_agent/cli.py +0 -428
  126. synth_ai/sdk/api/research_agent/config.py +0 -357
  127. synth_ai/sdk/api/research_agent/job.py +0 -717
  128. synth_ai/sdk/baseline/__init__.py +0 -25
  129. synth_ai/sdk/baseline/config.py +0 -209
  130. synth_ai/sdk/baseline/discovery.py +0 -216
  131. synth_ai/sdk/baseline/execution.py +0 -154
  132. synth_ai/sdk/judging/__init__.py +0 -15
  133. synth_ai/sdk/judging/base.py +0 -24
  134. synth_ai/sdk/judging/client.py +0 -191
  135. synth_ai/sdk/judging/types.py +0 -42
  136. synth_ai/sdk/research_agent/__init__.py +0 -34
  137. synth_ai/sdk/research_agent/container_builder.py +0 -328
  138. synth_ai/sdk/research_agent/container_spec.py +0 -198
  139. synth_ai/sdk/research_agent/defaults.py +0 -34
  140. synth_ai/sdk/research_agent/results_collector.py +0 -69
  141. synth_ai/sdk/specs/__init__.py +0 -46
  142. synth_ai/sdk/specs/dataclasses.py +0 -149
  143. synth_ai/sdk/specs/loader.py +0 -144
  144. synth_ai/sdk/specs/serializer.py +0 -199
  145. synth_ai/sdk/specs/validation.py +0 -250
  146. synth_ai/sdk/tracing/__init__.py +0 -39
  147. synth_ai/sdk/usage/__init__.py +0 -37
  148. synth_ai/sdk/usage/client.py +0 -171
  149. synth_ai/sdk/usage/models.py +0 -261
  150. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
  151. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
  152. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
  153. {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
+