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.
- gepa/__init__.py +5 -0
- prompt_opt/__init__.py +16 -0
- prompt_opt/adapters/__init__.py +18 -0
- prompt_opt/adapters/synth_container.py +170 -0
- prompt_opt/adapters/synth_offline.py +81 -0
- prompt_opt/dspy/__init__.py +5 -0
- prompt_opt/dspy/gepa.py +5 -0
- prompt_opt/dspy/miprov2.py +281 -0
- prompt_opt/gepa_ai_compat.py +225 -0
- prompt_opt/mipro.py +114 -0
- prompt_opt/sdk/__init__.py +2 -0
- prompt_opt/sdk/optimization/__init__.py +14 -0
- prompt_opt/sdk/optimization/internal/__init__.py +2 -0
- prompt_opt/sdk/optimization/internal/configs/__init__.py +16 -0
- prompt_opt/sdk/optimization/internal/configs/prompt_learning.py +230 -0
- prompt_opt/sdk/optimization/internal/configs/shared.py +12 -0
- prompt_opt/sdk/optimization/internal/prompt_learning.py +302 -0
- prompt_opt/sdk/optimization/internal/runtime.py +1465 -0
- prompt_opt/sdk/optimization/internal/utils.py +29 -0
- prompt_opt/sdk/optimization/models.py +71 -0
- prompt_opt/sdk/optimization/policy/__init__.py +6 -0
- prompt_opt/sdk/optimization/policy/v1.py +470 -0
- synth_optimizers-0.1.0.dist-info/METADATA +243 -0
- synth_optimizers-0.1.0.dist-info/RECORD +27 -0
- synth_optimizers-0.1.0.dist-info/WHEEL +5 -0
- synth_optimizers-0.1.0.dist-info/licenses/LICENSE +176 -0
- synth_optimizers-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""GEPA-compatible local optimizer facade backed by the local offline SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Protocol
|
|
8
|
+
|
|
9
|
+
from prompt_opt.adapters.synth_offline import SynthOfflineLearningAdapter
|
|
10
|
+
from prompt_opt.sdk.optimization.internal.prompt_learning import PromptLearningJob
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from synth_ai.gepa.core.result import GEPAResult as SynthGEPAResult
|
|
14
|
+
except Exception: # pragma: no cover - allows standalone use
|
|
15
|
+
SynthGEPAResult = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalGEPAAdapterProtocol(Protocol):
|
|
19
|
+
"""Protocol compatible with GEPA adapter-style calls."""
|
|
20
|
+
|
|
21
|
+
def evaluate(
|
|
22
|
+
self,
|
|
23
|
+
batch: list[Mapping[str, Any]],
|
|
24
|
+
candidate: dict[str, str],
|
|
25
|
+
capture_traces: bool = False,
|
|
26
|
+
) -> Any:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def make_reflective_dataset(
|
|
30
|
+
self,
|
|
31
|
+
candidate: dict[str, str],
|
|
32
|
+
eval_batch: Any,
|
|
33
|
+
components_to_update: list[str],
|
|
34
|
+
) -> Mapping[str, Sequence[Mapping[str, Any]]]:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class LocalGEPAResult:
|
|
40
|
+
"""Minimal GEPA-style result when synth_ai is unavailable."""
|
|
41
|
+
|
|
42
|
+
candidates: list[dict[str, str]]
|
|
43
|
+
parents: list[list[int | None]]
|
|
44
|
+
val_aggregate_scores: list[float]
|
|
45
|
+
val_subscores: list[dict[int, float]]
|
|
46
|
+
per_val_instance_best_candidates: dict[int, set[int]]
|
|
47
|
+
discovery_eval_counts: list[int]
|
|
48
|
+
total_metric_calls: int
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def best_idx(self) -> int:
|
|
52
|
+
return max(range(len(self.val_aggregate_scores)), key=self.val_aggregate_scores.__getitem__)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def best_candidate(self) -> dict[str, str]:
|
|
56
|
+
return self.candidates[self.best_idx]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def optimize(
|
|
60
|
+
seed_candidate: dict[str, str],
|
|
61
|
+
trainset: list[Mapping[str, Any]],
|
|
62
|
+
valset: list[Mapping[str, Any]] | None = None,
|
|
63
|
+
adapter: LocalGEPAAdapterProtocol | None = None,
|
|
64
|
+
task_lm: str | Any | None = None,
|
|
65
|
+
evaluator: Any | None = None,
|
|
66
|
+
reflection_lm: str | Any | None = None,
|
|
67
|
+
max_metric_calls: int | None = None,
|
|
68
|
+
stop_callbacks: Any | None = None,
|
|
69
|
+
**_: Any,
|
|
70
|
+
) -> Any:
|
|
71
|
+
"""Local GEPA-compatible optimize function.
|
|
72
|
+
|
|
73
|
+
This function is shaped for `gepa-ai` compatibility and can be used as a
|
|
74
|
+
drop-in local replacement where a simple adapter-based optimization loop is
|
|
75
|
+
sufficient.
|
|
76
|
+
"""
|
|
77
|
+
del task_lm, evaluator, reflection_lm, stop_callbacks
|
|
78
|
+
if not seed_candidate:
|
|
79
|
+
raise ValueError("seed_candidate must contain at least one entry.")
|
|
80
|
+
if not trainset:
|
|
81
|
+
raise ValueError("trainset must contain at least one item.")
|
|
82
|
+
if adapter is None:
|
|
83
|
+
raise ValueError("adapter is required for local/offline mode.")
|
|
84
|
+
|
|
85
|
+
eval_set: list[Mapping[str, Any]] = list(valset if valset is not None else trainset)
|
|
86
|
+
budget = max(1, int(max_metric_calls or 8))
|
|
87
|
+
stages = [
|
|
88
|
+
{
|
|
89
|
+
"id": key,
|
|
90
|
+
"name": key,
|
|
91
|
+
"messages": [{"role": "system", "pattern": value, "order": 0}],
|
|
92
|
+
"wildcards": {},
|
|
93
|
+
}
|
|
94
|
+
for key, value in seed_candidate.items()
|
|
95
|
+
]
|
|
96
|
+
if not stages:
|
|
97
|
+
stages = [
|
|
98
|
+
{
|
|
99
|
+
"id": "default",
|
|
100
|
+
"name": "default",
|
|
101
|
+
"messages": [{"role": "system", "pattern": "You are a helpful assistant.", "order": 0}],
|
|
102
|
+
"wildcards": {},
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
prompt_learning_config = {
|
|
107
|
+
"prompt_learning": {
|
|
108
|
+
"algorithm": "gepa",
|
|
109
|
+
"execution_mode": "retrieved",
|
|
110
|
+
"task_data": {
|
|
111
|
+
"train_examples": [dict(item) for item in trainset],
|
|
112
|
+
"validation_examples": [dict(item) for item in eval_set],
|
|
113
|
+
},
|
|
114
|
+
"gepa": {
|
|
115
|
+
"initial_candidate": {"stages": stages},
|
|
116
|
+
"population": {
|
|
117
|
+
"initial_size": 1,
|
|
118
|
+
"num_generations": 1,
|
|
119
|
+
"children_per_generation": max(1, budget - 1),
|
|
120
|
+
},
|
|
121
|
+
"termination_conditions": {
|
|
122
|
+
"total_rollouts": budget * max(1, len(eval_set)),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"local_runtime": {
|
|
126
|
+
"adapter": adapter,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
job = PromptLearningJob.from_dict(
|
|
131
|
+
prompt_learning_config,
|
|
132
|
+
backend_url="local://prompt-opt",
|
|
133
|
+
api_key="local",
|
|
134
|
+
)
|
|
135
|
+
job.submit()
|
|
136
|
+
job.stream_until_complete(timeout=300.0, interval=0.05).to_dict()
|
|
137
|
+
candidates_page = job.list_candidates(limit=budget + 1)
|
|
138
|
+
raw_candidates = candidates_page.get("items", [])
|
|
139
|
+
|
|
140
|
+
candidates: list[dict[str, str]] = []
|
|
141
|
+
parents: list[list[int | None]] = []
|
|
142
|
+
scores: list[float] = []
|
|
143
|
+
subscores: list[dict[int, float]] = []
|
|
144
|
+
eval_counts: list[int] = []
|
|
145
|
+
metric_calls_total = 0
|
|
146
|
+
candidate_index_by_id: dict[str, int] = {}
|
|
147
|
+
|
|
148
|
+
seed_eval_items = job.get_state_envelope().get("state", {}).get("seed_evals", [])
|
|
149
|
+
per_candidate_per_seed: dict[str, dict[int, float]] = {}
|
|
150
|
+
for seed_eval in seed_eval_items:
|
|
151
|
+
if not isinstance(seed_eval, dict):
|
|
152
|
+
continue
|
|
153
|
+
candidate_id = str(seed_eval.get("candidate_id", ""))
|
|
154
|
+
seed = int(seed_eval.get("seed", 0))
|
|
155
|
+
reward = float(seed_eval.get("reward", 0.0))
|
|
156
|
+
per_candidate_per_seed.setdefault(candidate_id, {})[seed] = reward
|
|
157
|
+
|
|
158
|
+
for raw_candidate in raw_candidates:
|
|
159
|
+
if not isinstance(raw_candidate, dict):
|
|
160
|
+
continue
|
|
161
|
+
stage_items = raw_candidate.get("stages") or raw_candidate.get("candidate", {}).get("stages") or []
|
|
162
|
+
candidate_map: dict[str, str] = {}
|
|
163
|
+
for stage in stage_items:
|
|
164
|
+
if not isinstance(stage, dict):
|
|
165
|
+
continue
|
|
166
|
+
stage_key = str(stage.get("id") or stage.get("name") or f"stage_{len(candidate_map)}")
|
|
167
|
+
for message in stage.get("messages", []):
|
|
168
|
+
if isinstance(message, dict) and message.get("role") == "system":
|
|
169
|
+
text = message.get("pattern") or message.get("content")
|
|
170
|
+
if isinstance(text, str):
|
|
171
|
+
candidate_map[stage_key] = text
|
|
172
|
+
break
|
|
173
|
+
if not candidate_map:
|
|
174
|
+
candidate_map = {
|
|
175
|
+
"default": str(raw_candidate.get("candidate_content", "")),
|
|
176
|
+
}
|
|
177
|
+
candidate_id = str(raw_candidate.get("candidate_id", ""))
|
|
178
|
+
candidate_index_by_id[candidate_id] = len(candidates)
|
|
179
|
+
candidates.append(candidate_map)
|
|
180
|
+
parent_id = raw_candidate.get("parent_id")
|
|
181
|
+
parents.append([candidate_index_by_id[parent_id]] if isinstance(parent_id, str) and parent_id in candidate_index_by_id else [None])
|
|
182
|
+
candidate_subscores = per_candidate_per_seed.get(candidate_id, {})
|
|
183
|
+
subscores.append(dict(candidate_subscores))
|
|
184
|
+
score = float(raw_candidate.get("avg_reward", 0.0) or 0.0)
|
|
185
|
+
scores.append(score)
|
|
186
|
+
eval_counts.append(len(candidate_subscores))
|
|
187
|
+
metric_calls_total += len(candidate_subscores)
|
|
188
|
+
|
|
189
|
+
per_instance_best: dict[int, set[int]] = {}
|
|
190
|
+
for example_idx in range(len(eval_set)):
|
|
191
|
+
best_score = max((item.get(example_idx, float("-inf")) for item in subscores), default=float("-inf"))
|
|
192
|
+
per_instance_best[example_idx] = {
|
|
193
|
+
candidate_idx
|
|
194
|
+
for candidate_idx, item in enumerate(subscores)
|
|
195
|
+
if item.get(example_idx, float("-inf")) == best_score
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if SynthGEPAResult is not None:
|
|
199
|
+
return SynthGEPAResult(
|
|
200
|
+
candidates=candidates,
|
|
201
|
+
parents=parents,
|
|
202
|
+
val_aggregate_scores=scores,
|
|
203
|
+
val_subscores=subscores,
|
|
204
|
+
per_val_instance_best_candidates=per_instance_best,
|
|
205
|
+
discovery_eval_counts=eval_counts,
|
|
206
|
+
total_metric_calls=metric_calls_total,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return LocalGEPAResult(
|
|
210
|
+
candidates=candidates,
|
|
211
|
+
parents=parents,
|
|
212
|
+
val_aggregate_scores=scores,
|
|
213
|
+
val_subscores=subscores,
|
|
214
|
+
per_val_instance_best_candidates=per_instance_best,
|
|
215
|
+
discovery_eval_counts=eval_counts,
|
|
216
|
+
total_metric_calls=metric_calls_total,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = [
|
|
221
|
+
"LocalGEPAAdapterProtocol",
|
|
222
|
+
"LocalGEPAResult",
|
|
223
|
+
"SynthOfflineLearningAdapter",
|
|
224
|
+
"optimize",
|
|
225
|
+
]
|
prompt_opt/mipro.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Local MIPRO entrypoint backed by the mirrored offline SDK runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from prompt_opt.sdk.optimization.internal.prompt_learning import PromptLearningJob
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_PROPOSER_BACKENDS = ["single_prompt", "rlm"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def proposer_backends() -> list[str]:
|
|
14
|
+
"""Return the supported public proposer backend tokens."""
|
|
15
|
+
return list(_PROPOSER_BACKENDS)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _policy_to_initial_candidate(initial_policy: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
template = str(initial_policy.get("template", "")).strip() or "You are a helpful assistant."
|
|
20
|
+
return {
|
|
21
|
+
"stages": [
|
|
22
|
+
{
|
|
23
|
+
"id": "default",
|
|
24
|
+
"name": "Default",
|
|
25
|
+
"messages": [
|
|
26
|
+
{"role": "system", "pattern": template, "order": 0},
|
|
27
|
+
{"role": "user", "pattern": "{input}", "order": 1},
|
|
28
|
+
],
|
|
29
|
+
"wildcards": {},
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_best_policy(result_payload: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
best_candidate = result_payload.get("best_candidate")
|
|
37
|
+
if not isinstance(best_candidate, dict):
|
|
38
|
+
return {"template": ""}
|
|
39
|
+
stages = best_candidate.get("stages") or best_candidate.get("candidate", {}).get("stages") or []
|
|
40
|
+
for stage in stages:
|
|
41
|
+
if not isinstance(stage, dict):
|
|
42
|
+
continue
|
|
43
|
+
for message in stage.get("messages", []):
|
|
44
|
+
if not isinstance(message, dict):
|
|
45
|
+
continue
|
|
46
|
+
if message.get("role") == "system":
|
|
47
|
+
text = message.get("pattern") or message.get("content")
|
|
48
|
+
if isinstance(text, str):
|
|
49
|
+
return {"template": text}
|
|
50
|
+
content = best_candidate.get("candidate_content")
|
|
51
|
+
if isinstance(content, str):
|
|
52
|
+
return {"template": content}
|
|
53
|
+
return {"template": ""}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_mipro(
|
|
57
|
+
*,
|
|
58
|
+
config: dict[str, Any],
|
|
59
|
+
initial_policy: dict[str, Any],
|
|
60
|
+
dataset: dict[str, Any],
|
|
61
|
+
task_llm: Callable[[str], str],
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Run local offline MIPRO and return a JSON-compatible result payload."""
|
|
64
|
+
examples = []
|
|
65
|
+
for item in dataset.get("examples", []):
|
|
66
|
+
if isinstance(item, dict):
|
|
67
|
+
examples.append(
|
|
68
|
+
{
|
|
69
|
+
"input": item.get("input"),
|
|
70
|
+
"answer": item.get("expected"),
|
|
71
|
+
"expected": item.get("expected"),
|
|
72
|
+
"metadata": dict(item.get("metadata") or {}),
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
prompt_learning_config = {
|
|
76
|
+
"prompt_learning": {
|
|
77
|
+
"algorithm": "mipro",
|
|
78
|
+
"execution_mode": "retrieved",
|
|
79
|
+
"task_data": {
|
|
80
|
+
"examples": examples,
|
|
81
|
+
},
|
|
82
|
+
"mipro": {
|
|
83
|
+
"initial_candidate": _policy_to_initial_candidate(initial_policy),
|
|
84
|
+
"num_candidates": int(config.get("num_candidates", 8)),
|
|
85
|
+
"max_iterations": int(config.get("max_iterations", 8)),
|
|
86
|
+
"early_stop_rounds": int(config.get("early_stop_rounds", 3)),
|
|
87
|
+
"min_improvement": float(config.get("min_improvement", 1e-6)),
|
|
88
|
+
"seed": int(config.get("seed", 0)),
|
|
89
|
+
"proposer_backend": str(config.get("proposer_backend", "single_prompt")),
|
|
90
|
+
"termination_conditions": {
|
|
91
|
+
"total_rollouts": int(config.get("max_iterations", 8)) * max(1, len(examples)),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"local_runtime": {
|
|
95
|
+
"task_model": task_llm,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
job = PromptLearningJob.from_dict(
|
|
101
|
+
prompt_learning_config,
|
|
102
|
+
backend_url="local://prompt-opt",
|
|
103
|
+
api_key="local",
|
|
104
|
+
)
|
|
105
|
+
job_id = job.submit()
|
|
106
|
+
result = job.stream_until_complete(timeout=300.0, interval=0.05).to_dict()
|
|
107
|
+
return {
|
|
108
|
+
"run_id": job_id,
|
|
109
|
+
"initial_policy": dict(initial_policy),
|
|
110
|
+
"best_policy": _extract_best_policy(result),
|
|
111
|
+
"best_score": result.get("best_reward"),
|
|
112
|
+
"history": job.get_results().get("result", {}).get("history", []),
|
|
113
|
+
"job_result": result,
|
|
114
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Local offline prompt optimization SDK."""
|
|
2
|
+
|
|
3
|
+
from .models import PolicyCandidate, PolicyCandidatePage, PromptLearningResult
|
|
4
|
+
from .policy.v1 import PolicyOptimizationOfflineJob
|
|
5
|
+
from .internal.prompt_learning import PromptLearningJob, PromptLearningJobConfig
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"PolicyCandidate",
|
|
9
|
+
"PolicyCandidatePage",
|
|
10
|
+
"PolicyOptimizationOfflineJob",
|
|
11
|
+
"PromptLearningJob",
|
|
12
|
+
"PromptLearningJobConfig",
|
|
13
|
+
"PromptLearningResult",
|
|
14
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Mirrored prompt-learning config models."""
|
|
2
|
+
|
|
3
|
+
from .prompt_learning import (
|
|
4
|
+
GEPAConfig,
|
|
5
|
+
MIPROAlgorithmConfig,
|
|
6
|
+
PromptLearningConfig,
|
|
7
|
+
PromptLearningPolicyConfig,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"GEPAConfig",
|
|
12
|
+
"MIPROAlgorithmConfig",
|
|
13
|
+
"PromptLearningConfig",
|
|
14
|
+
"PromptLearningPolicyConfig",
|
|
15
|
+
]
|
|
16
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Canonical prompt-learning config models for local offline prompt-opt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, field_validator, model_validator
|
|
10
|
+
|
|
11
|
+
from ..utils import load_toml
|
|
12
|
+
from .shared import ExtraModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PromptLearningPolicyConfig(ExtraModel):
|
|
16
|
+
provider: str | None = None
|
|
17
|
+
inference_url: str | None = None
|
|
18
|
+
inference_mode: str | None = None
|
|
19
|
+
temperature: float = 0.0
|
|
20
|
+
max_completion_tokens: int = 512
|
|
21
|
+
policy_name: str | None = None
|
|
22
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
context_override: dict[str, Any] | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MessagePatternConfig(ExtraModel):
|
|
27
|
+
role: str
|
|
28
|
+
pattern: str
|
|
29
|
+
order: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PromptStageConfig(ExtraModel):
|
|
33
|
+
id: str | None = None
|
|
34
|
+
name: str | None = None
|
|
35
|
+
messages: list[MessagePatternConfig] = Field(default_factory=list)
|
|
36
|
+
wildcards: dict[str, str] = Field(default_factory=dict)
|
|
37
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PromptCandidateConfig(ExtraModel):
|
|
41
|
+
stages: list[PromptStageConfig] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TrainPoolsConfig(ExtraModel):
|
|
45
|
+
reflection_seeds: list[int] = Field(default_factory=list)
|
|
46
|
+
pareto_seeds: list[int] = Field(default_factory=list)
|
|
47
|
+
seeds: list[int] = Field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TaskDataConfig(ExtraModel):
|
|
51
|
+
split: str | None = None
|
|
52
|
+
train_pools: TrainPoolsConfig | dict[str, Any] | None = None
|
|
53
|
+
validation_seeds: list[int] = Field(default_factory=list)
|
|
54
|
+
validation_examples: list[dict[str, Any]] = Field(default_factory=list)
|
|
55
|
+
train_examples: list[dict[str, Any]] = Field(default_factory=list)
|
|
56
|
+
examples: list[dict[str, Any]] = Field(default_factory=list)
|
|
57
|
+
examples_by_seed: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
@field_validator("train_pools", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def _coerce_train_pools(cls, value: Any) -> Any:
|
|
62
|
+
if value is None or isinstance(value, TrainPoolsConfig):
|
|
63
|
+
return value
|
|
64
|
+
if isinstance(value, Mapping):
|
|
65
|
+
return TrainPoolsConfig.model_validate(dict(value))
|
|
66
|
+
raise ValueError("train_pools must be a mapping when provided")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PopulationConfig(ExtraModel):
|
|
70
|
+
initial_size: int = 1
|
|
71
|
+
num_generations: int = 1
|
|
72
|
+
children_per_generation: int = 4
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TerminationConditionsConfig(ExtraModel):
|
|
76
|
+
total_rollouts: int | None = None
|
|
77
|
+
max_iterations: int | None = None
|
|
78
|
+
patience: int | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GEPAConfig(ExtraModel):
|
|
82
|
+
env_name: str | None = None
|
|
83
|
+
initial_candidate: PromptCandidateConfig | dict[str, Any] | None = None
|
|
84
|
+
termination_conditions: TerminationConditionsConfig | dict[str, Any] | None = None
|
|
85
|
+
mode: str | None = None
|
|
86
|
+
execution_mode: str | None = None
|
|
87
|
+
proposer_backend: str | None = None
|
|
88
|
+
population: PopulationConfig | dict[str, Any] | None = None
|
|
89
|
+
modules: list[dict[str, Any]] | None = None
|
|
90
|
+
|
|
91
|
+
@field_validator("initial_candidate", mode="before")
|
|
92
|
+
@classmethod
|
|
93
|
+
def _coerce_initial_candidate(cls, value: Any) -> Any:
|
|
94
|
+
if value is None or isinstance(value, PromptCandidateConfig):
|
|
95
|
+
return value
|
|
96
|
+
if isinstance(value, Mapping):
|
|
97
|
+
return PromptCandidateConfig.model_validate(dict(value))
|
|
98
|
+
raise ValueError("initial_candidate must be a mapping when provided")
|
|
99
|
+
|
|
100
|
+
@field_validator("termination_conditions", mode="before")
|
|
101
|
+
@classmethod
|
|
102
|
+
def _coerce_termination(cls, value: Any) -> Any:
|
|
103
|
+
if value is None or isinstance(value, TerminationConditionsConfig):
|
|
104
|
+
return value
|
|
105
|
+
if isinstance(value, Mapping):
|
|
106
|
+
return TerminationConditionsConfig.model_validate(dict(value))
|
|
107
|
+
raise ValueError("termination_conditions must be a mapping when provided")
|
|
108
|
+
|
|
109
|
+
@field_validator("population", mode="before")
|
|
110
|
+
@classmethod
|
|
111
|
+
def _coerce_population(cls, value: Any) -> Any:
|
|
112
|
+
if value is None or isinstance(value, PopulationConfig):
|
|
113
|
+
return value
|
|
114
|
+
if isinstance(value, Mapping):
|
|
115
|
+
return PopulationConfig.model_validate(dict(value))
|
|
116
|
+
raise ValueError("population must be a mapping when provided")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class MIPROAlgorithmConfig(ExtraModel):
|
|
120
|
+
initial_candidate: PromptCandidateConfig | dict[str, Any] | None = None
|
|
121
|
+
execution_mode: str | None = None
|
|
122
|
+
mode: str | None = None
|
|
123
|
+
num_candidates: int = 8
|
|
124
|
+
max_iterations: int = 8
|
|
125
|
+
early_stop_rounds: int = 3
|
|
126
|
+
min_improvement: float = 1e-6
|
|
127
|
+
proposer_backend: str = "single_prompt"
|
|
128
|
+
seed: int = 0
|
|
129
|
+
termination_conditions: TerminationConditionsConfig | dict[str, Any] | None = None
|
|
130
|
+
|
|
131
|
+
@field_validator("initial_candidate", mode="before")
|
|
132
|
+
@classmethod
|
|
133
|
+
def _coerce_initial_candidate(cls, value: Any) -> Any:
|
|
134
|
+
if value is None or isinstance(value, PromptCandidateConfig):
|
|
135
|
+
return value
|
|
136
|
+
if isinstance(value, Mapping):
|
|
137
|
+
return PromptCandidateConfig.model_validate(dict(value))
|
|
138
|
+
raise ValueError("initial_candidate must be a mapping when provided")
|
|
139
|
+
|
|
140
|
+
@field_validator("termination_conditions", mode="before")
|
|
141
|
+
@classmethod
|
|
142
|
+
def _coerce_termination(cls, value: Any) -> Any:
|
|
143
|
+
if value is None or isinstance(value, TerminationConditionsConfig):
|
|
144
|
+
return value
|
|
145
|
+
if isinstance(value, Mapping):
|
|
146
|
+
return TerminationConditionsConfig.model_validate(dict(value))
|
|
147
|
+
raise ValueError("termination_conditions must be a mapping when provided")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PromptLearningConfig(ExtraModel):
|
|
151
|
+
algorithm: str
|
|
152
|
+
job_kind: str | None = None
|
|
153
|
+
algorithm_name: str | None = None
|
|
154
|
+
execution_mode: str | None = None
|
|
155
|
+
config_schema_version: str | None = None
|
|
156
|
+
container_url: str | None = None
|
|
157
|
+
container_id: str | None = None
|
|
158
|
+
task_data: TaskDataConfig | dict[str, Any] | None = None
|
|
159
|
+
policy: PromptLearningPolicyConfig | dict[str, Any] | None = None
|
|
160
|
+
gepa: GEPAConfig | None = None
|
|
161
|
+
mipro: MIPROAlgorithmConfig | dict[str, Any] | None = None
|
|
162
|
+
verifier: dict[str, Any] | None = None
|
|
163
|
+
proxy_models: dict[str, Any] | None = None
|
|
164
|
+
env_config: dict[str, Any] | None = None
|
|
165
|
+
use_byok: bool | None = None
|
|
166
|
+
|
|
167
|
+
@field_validator("task_data", mode="before")
|
|
168
|
+
@classmethod
|
|
169
|
+
def _coerce_task_data(cls, value: Any) -> Any:
|
|
170
|
+
if value is None or isinstance(value, TaskDataConfig):
|
|
171
|
+
return value
|
|
172
|
+
if isinstance(value, Mapping):
|
|
173
|
+
return TaskDataConfig.model_validate(dict(value))
|
|
174
|
+
raise ValueError("task_data must be a mapping when provided")
|
|
175
|
+
|
|
176
|
+
@field_validator("policy", mode="before")
|
|
177
|
+
@classmethod
|
|
178
|
+
def _coerce_policy(cls, value: Any) -> Any:
|
|
179
|
+
if value is None or isinstance(value, PromptLearningPolicyConfig):
|
|
180
|
+
return value
|
|
181
|
+
if isinstance(value, Mapping):
|
|
182
|
+
return PromptLearningPolicyConfig.model_validate(dict(value))
|
|
183
|
+
raise ValueError("policy must be a mapping when provided")
|
|
184
|
+
|
|
185
|
+
@field_validator("mipro", mode="before")
|
|
186
|
+
@classmethod
|
|
187
|
+
def _coerce_mipro(cls, value: Any) -> Any:
|
|
188
|
+
if value is None or isinstance(value, MIPROAlgorithmConfig):
|
|
189
|
+
return value
|
|
190
|
+
if isinstance(value, Mapping):
|
|
191
|
+
return MIPROAlgorithmConfig.model_validate(dict(value))
|
|
192
|
+
raise ValueError("mipro must be a mapping when provided")
|
|
193
|
+
|
|
194
|
+
@model_validator(mode="after")
|
|
195
|
+
def _normalize_execution_mode(self) -> "PromptLearningConfig":
|
|
196
|
+
if self.algorithm_name is None:
|
|
197
|
+
self.algorithm_name = self.algorithm
|
|
198
|
+
if self.job_kind is None:
|
|
199
|
+
self.job_kind = "optimization"
|
|
200
|
+
|
|
201
|
+
algorithm_mode = None
|
|
202
|
+
if self.algorithm == "gepa" and self.gepa is not None:
|
|
203
|
+
algorithm_mode = self.gepa.execution_mode or self.gepa.mode
|
|
204
|
+
elif self.algorithm == "mipro" and self.mipro is not None:
|
|
205
|
+
algorithm_mode = self.mipro.execution_mode or self.mipro.mode
|
|
206
|
+
|
|
207
|
+
effective = self.execution_mode or algorithm_mode or "offline"
|
|
208
|
+
if effective == "proxied":
|
|
209
|
+
effective = "retrieved"
|
|
210
|
+
self.execution_mode = effective
|
|
211
|
+
|
|
212
|
+
if self.algorithm == "gepa" and self.gepa is not None:
|
|
213
|
+
self.gepa.execution_mode = effective
|
|
214
|
+
if self.algorithm == "mipro" and self.mipro is not None:
|
|
215
|
+
self.mipro.execution_mode = effective
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def from_mapping(cls, data: Mapping[str, Any]) -> "PromptLearningConfig":
|
|
220
|
+
payload = dict(data)
|
|
221
|
+
if "prompt_learning" in payload and isinstance(payload["prompt_learning"], Mapping):
|
|
222
|
+
payload = dict(payload["prompt_learning"])
|
|
223
|
+
return cls.model_validate(payload)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def from_path(cls, path: str | Path) -> "PromptLearningConfig":
|
|
227
|
+
return cls.from_mapping(load_toml(path))
|
|
228
|
+
|
|
229
|
+
def to_dict(self) -> dict[str, Any]:
|
|
230
|
+
return self.model_dump(mode="python", exclude_none=True)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shared config base models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExtraModel(BaseModel):
|
|
9
|
+
"""Base model that preserves unknown keys for forward compatibility."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="allow")
|
|
12
|
+
|