synth-optimizers 0.1.0__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.
@@ -0,0 +1,302 @@
1
+ """High-level local prompt-learning job wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..models import PolicyCandidate, PolicyCandidatePage, PromptLearningResult
10
+ from .configs.prompt_learning import PromptLearningConfig
11
+ from .runtime import RUNTIME, get_candidate_typed, list_candidates_typed
12
+ from .utils import load_toml, run_sync
13
+ from ..policy.v1 import PolicyOptimizationOfflineJob
14
+
15
+
16
+ @dataclass
17
+ class PromptLearningJobConfig:
18
+ """Configuration for a local prompt-learning job."""
19
+
20
+ backend_url: str
21
+ api_key: str
22
+ config_path: Path | None = None
23
+ config_dict: dict[str, Any] | None = None
24
+ container_api_key: str | None = field(default=None, repr=False)
25
+ container_key: str | None = field(default=None, repr=False)
26
+ container_worker_token: str | None = field(default=None, repr=False)
27
+ allow_experimental: bool | None = None
28
+ overrides: dict[str, Any] | None = None
29
+
30
+ def __post_init__(self) -> None:
31
+ has_path = self.config_path is not None
32
+ has_dict = self.config_dict is not None
33
+ if has_path == has_dict:
34
+ raise ValueError("Provide exactly one of config_path or config_dict")
35
+ if self.config_path is not None and not self.config_path.exists():
36
+ raise FileNotFoundError(f"Config file not found: {self.config_path}")
37
+ if not self.backend_url:
38
+ raise ValueError("backend_url is required")
39
+ if not self.api_key:
40
+ raise ValueError("api_key is required")
41
+ if self.container_api_key is None and isinstance(self.container_key, str):
42
+ stripped = self.container_key.strip()
43
+ self.container_api_key = stripped or None
44
+
45
+ def materialize(self) -> dict[str, Any]:
46
+ payload = load_toml(self.config_path) if self.config_path is not None else dict(self.config_dict or {})
47
+ if self.overrides:
48
+ payload = _merge_overrides(payload, dict(self.overrides))
49
+ return payload
50
+
51
+
52
+ def _merge_overrides(payload: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
53
+ merged = dict(payload)
54
+ for dotted_key, value in overrides.items():
55
+ current = merged
56
+ parts = str(dotted_key).split(".")
57
+ for part in parts[:-1]:
58
+ existing = current.get(part)
59
+ if not isinstance(existing, dict):
60
+ existing = {}
61
+ current[part] = existing
62
+ current = existing
63
+ current[parts[-1]] = value
64
+ return merged
65
+
66
+
67
+ class PromptLearningJobPoller:
68
+ """Small local poller."""
69
+
70
+ def poll_job(self, job_id: str) -> dict[str, Any]:
71
+ return RUNTIME.get_job(job_id).status_payload()
72
+
73
+
74
+ class PromptLearningJob:
75
+ """High-level local prompt-learning job."""
76
+
77
+ def __init__(
78
+ self,
79
+ config: PromptLearningJobConfig,
80
+ job_id: str | None = None,
81
+ skip_health_check: bool = False,
82
+ ) -> None:
83
+ self.config = config
84
+ self._job_id = job_id
85
+ self._skip_health_check = skip_health_check
86
+ self._offline_job: PolicyOptimizationOfflineJob | None = None
87
+
88
+ @classmethod
89
+ def from_config(
90
+ cls,
91
+ config_path: str | Path,
92
+ backend_url: str | None = None,
93
+ api_key: str | None = None,
94
+ container_worker_token: str | None = None,
95
+ allow_experimental: bool | None = None,
96
+ overrides: dict[str, Any] | None = None,
97
+ ) -> "PromptLearningJob":
98
+ return cls(
99
+ PromptLearningJobConfig(
100
+ config_path=Path(config_path),
101
+ backend_url=str(backend_url or "local://prompt-opt"),
102
+ api_key=str(api_key or "local"),
103
+ container_worker_token=container_worker_token,
104
+ allow_experimental=allow_experimental,
105
+ overrides=overrides,
106
+ )
107
+ )
108
+
109
+ @classmethod
110
+ def from_dict(
111
+ cls,
112
+ config_dict: dict[str, Any],
113
+ backend_url: str | None = None,
114
+ api_key: str | None = None,
115
+ container_worker_token: str | None = None,
116
+ allow_experimental: bool | None = None,
117
+ overrides: dict[str, Any] | None = None,
118
+ skip_health_check: bool = False,
119
+ ) -> "PromptLearningJob":
120
+ return cls(
121
+ PromptLearningJobConfig(
122
+ config_dict=dict(config_dict),
123
+ backend_url=str(backend_url or "local://prompt-opt"),
124
+ api_key=str(api_key or "local"),
125
+ container_worker_token=container_worker_token,
126
+ allow_experimental=allow_experimental,
127
+ overrides=overrides,
128
+ ),
129
+ skip_health_check=skip_health_check,
130
+ )
131
+
132
+ @classmethod
133
+ def from_job_id(
134
+ cls,
135
+ job_id: str,
136
+ backend_url: str | None = None,
137
+ api_key: str | None = None,
138
+ ) -> "PromptLearningJob":
139
+ config = PromptLearningJobConfig(
140
+ config_dict={"prompt_learning": {"algorithm": "gepa"}},
141
+ backend_url=str(backend_url or "local://prompt-opt"),
142
+ api_key=str(api_key or "local"),
143
+ )
144
+ return cls(config=config, job_id=job_id)
145
+
146
+ def _kind_from_payload(self, payload: dict[str, Any]) -> str:
147
+ parsed = PromptLearningConfig.from_mapping(payload)
148
+ return "mipro_offline" if parsed.algorithm == "mipro" else "gepa_offline"
149
+
150
+ def submit(self) -> str:
151
+ if self._job_id is not None:
152
+ raise RuntimeError(f"Job already submitted: {self._job_id}")
153
+ payload = self.config.materialize()
154
+ system_name = _resolve_system_name(payload)
155
+ offline_job = PolicyOptimizationOfflineJob.create(
156
+ kind=self._kind_from_payload(payload),
157
+ technique="discrete_optimization",
158
+ system_name=system_name,
159
+ config=payload,
160
+ metadata={"skip_health_check": self._skip_health_check},
161
+ backend_url=self.config.backend_url,
162
+ api_key=self.config.api_key,
163
+ )
164
+ self._offline_job = offline_job
165
+ self._job_id = offline_job.job_id
166
+ return offline_job.job_id
167
+
168
+ @property
169
+ def job_id(self) -> str | None:
170
+ return self._job_id
171
+
172
+ def _ensure_offline_job(self) -> PolicyOptimizationOfflineJob:
173
+ if self._job_id is None:
174
+ raise RuntimeError("Job not yet submitted. Call submit() first.")
175
+ if self._offline_job is None:
176
+ self._offline_job = PolicyOptimizationOfflineJob.get(
177
+ self._job_id,
178
+ backend_url=self.config.backend_url,
179
+ api_key=self.config.api_key,
180
+ )
181
+ return self._offline_job
182
+
183
+ async def get_status_async(self) -> dict[str, Any]:
184
+ return await self._ensure_offline_job().status_async()
185
+
186
+ def get_status(self) -> dict[str, Any]:
187
+ return run_sync(self.get_status_async())
188
+
189
+ async def stream_until_complete_async(
190
+ self,
191
+ *,
192
+ timeout: float = 3600.0,
193
+ interval: float = 15.0,
194
+ handlers: list[Any] | None = None,
195
+ on_event: Any | None = None,
196
+ ) -> PromptLearningResult:
197
+ payload = await self._ensure_offline_job().stream_until_complete_async(
198
+ timeout=timeout,
199
+ interval=interval,
200
+ handlers=handlers,
201
+ on_event=on_event,
202
+ )
203
+ return PromptLearningResult.from_response(self._job_id or "", payload)
204
+
205
+ def stream_until_complete(
206
+ self,
207
+ *,
208
+ timeout: float = 3600.0,
209
+ interval: float = 15.0,
210
+ handlers: list[Any] | None = None,
211
+ on_event: Any | None = None,
212
+ ) -> PromptLearningResult:
213
+ return run_sync(
214
+ self.stream_until_complete_async(
215
+ timeout=timeout,
216
+ interval=interval,
217
+ handlers=handlers,
218
+ on_event=on_event,
219
+ )
220
+ )
221
+
222
+ async def get_results_async(self) -> dict[str, Any]:
223
+ return dict(RUNTIME.get_job(self._job_id or "").result_payload)
224
+
225
+ def get_results(self) -> dict[str, Any]:
226
+ return run_sync(self.get_results_async())
227
+
228
+ async def list_candidates_async(
229
+ self,
230
+ *,
231
+ algorithm: str | None = None,
232
+ mode: str | None = None,
233
+ status: str | None = None,
234
+ limit: int = 100,
235
+ cursor: str | None = None,
236
+ sort: str | None = None,
237
+ include: str | None = None,
238
+ ) -> dict[str, Any]:
239
+ del algorithm, mode, cursor, sort, include
240
+ return RUNTIME.list_candidates(self._job_id or "", limit=limit, status=status)
241
+
242
+ async def list_candidates_typed_async(
243
+ self,
244
+ *,
245
+ algorithm: str | None = None,
246
+ mode: str | None = None,
247
+ status: str | None = None,
248
+ limit: int = 100,
249
+ cursor: str | None = None,
250
+ sort: str | None = None,
251
+ include: str | None = None,
252
+ ) -> PolicyCandidatePage:
253
+ del algorithm, mode, cursor, sort, include
254
+ return list_candidates_typed(self._job_id or "", limit=limit, status=status)
255
+
256
+ def list_candidates(self, **kwargs: Any) -> dict[str, Any]:
257
+ return run_sync(self.list_candidates_async(**kwargs))
258
+
259
+ def list_candidates_typed(self, **kwargs: Any) -> PolicyCandidatePage:
260
+ return run_sync(self.list_candidates_typed_async(**kwargs))
261
+
262
+ async def get_candidate_async(self, candidate_id: str) -> dict[str, Any]:
263
+ return RUNTIME.get_candidate(self._job_id or "", candidate_id)
264
+
265
+ async def get_candidate_typed_async(self, candidate_id: str) -> PolicyCandidate:
266
+ return get_candidate_typed(self._job_id or "", candidate_id)
267
+
268
+ def get_candidate(self, candidate_id: str) -> dict[str, Any]:
269
+ return run_sync(self.get_candidate_async(candidate_id))
270
+
271
+ def get_candidate_typed(self, candidate_id: str) -> PolicyCandidate:
272
+ return run_sync(self.get_candidate_typed_async(candidate_id))
273
+
274
+ async def submit_candidates_async(self, **kwargs: Any) -> dict[str, Any]:
275
+ return await self._ensure_offline_job().submit_candidates_async(**kwargs)
276
+
277
+ def submit_candidates(self, **kwargs: Any) -> dict[str, Any]:
278
+ return run_sync(self.submit_candidates_async(**kwargs))
279
+
280
+ async def get_state_baseline_info_async(self) -> dict[str, Any]:
281
+ return await self._ensure_offline_job().get_state_baseline_info_async()
282
+
283
+ def get_state_baseline_info(self) -> dict[str, Any]:
284
+ return run_sync(self.get_state_baseline_info_async())
285
+
286
+ async def get_state_envelope_async(self) -> dict[str, Any]:
287
+ return await self._ensure_offline_job().get_state_envelope_async()
288
+
289
+ def get_state_envelope(self) -> dict[str, Any]:
290
+ return run_sync(self.get_state_envelope_async())
291
+
292
+
293
+ def _resolve_system_name(payload: dict[str, Any]) -> str:
294
+ top_level = payload.get("prompt_learning", payload)
295
+ if isinstance(top_level, dict):
296
+ system_name = top_level.get("system_name") or top_level.get("env_name")
297
+ if isinstance(system_name, str) and system_name.strip():
298
+ return system_name.strip()
299
+ algorithm = top_level.get("algorithm")
300
+ if isinstance(algorithm, str) and algorithm.strip():
301
+ return f"{algorithm.strip()}-local-job"
302
+ return "prompt-learning-local-job"