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
gepa/__init__.py
ADDED
prompt_opt/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""synth-optimizers public API exposed through the `prompt_opt` package."""
|
|
2
|
+
|
|
3
|
+
from .gepa_ai_compat import LocalGEPAAdapterProtocol, optimize
|
|
4
|
+
from .mipro import proposer_backends, run_mipro
|
|
5
|
+
from .dspy.miprov2 import MIPROv2
|
|
6
|
+
from .sdk.optimization import PolicyOptimizationOfflineJob, PromptLearningJob
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"LocalGEPAAdapterProtocol",
|
|
10
|
+
"MIPROv2",
|
|
11
|
+
"PolicyOptimizationOfflineJob",
|
|
12
|
+
"PromptLearningJob",
|
|
13
|
+
"optimize",
|
|
14
|
+
"proposer_backends",
|
|
15
|
+
"run_mipro",
|
|
16
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Adapter implementations for prompt-opt."""
|
|
2
|
+
|
|
3
|
+
from .synth_container import (
|
|
4
|
+
ContainerEvaluationBatch,
|
|
5
|
+
SynthContainerLearningAdapter,
|
|
6
|
+
default_rollout_request_builder,
|
|
7
|
+
default_rollout_score_extractor,
|
|
8
|
+
)
|
|
9
|
+
from .synth_offline import LocalEvaluator, SynthOfflineLearningAdapter
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ContainerEvaluationBatch",
|
|
13
|
+
"LocalEvaluator",
|
|
14
|
+
"SynthContainerLearningAdapter",
|
|
15
|
+
"SynthOfflineLearningAdapter",
|
|
16
|
+
"default_rollout_request_builder",
|
|
17
|
+
"default_rollout_score_extractor",
|
|
18
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Synth Container adapter for local prompt-opt evaluations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib import request
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RolloutRequestBuilder = Callable[[Mapping[str, Any], dict[str, str], str], dict[str, Any]]
|
|
14
|
+
RolloutScoreExtractor = Callable[[dict[str, Any]], float]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def default_rollout_request_builder(
|
|
18
|
+
*,
|
|
19
|
+
example: Mapping[str, Any],
|
|
20
|
+
candidate: dict[str, str],
|
|
21
|
+
trace_correlation_id: str,
|
|
22
|
+
env_name: str = "prompt-opt-local",
|
|
23
|
+
seed: int | None = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Build a minimal Synth rollout request using the canonical container contract."""
|
|
26
|
+
env_seed = seed
|
|
27
|
+
if env_seed is None:
|
|
28
|
+
raw_seed = example.get("seed")
|
|
29
|
+
if isinstance(raw_seed, int):
|
|
30
|
+
env_seed = raw_seed
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"trace_correlation_id": trace_correlation_id,
|
|
34
|
+
"env": {
|
|
35
|
+
"env_name": env_name,
|
|
36
|
+
"seed": env_seed,
|
|
37
|
+
"config": {
|
|
38
|
+
"example": dict(example),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"policy": {
|
|
42
|
+
"policy_name": "prompt-opt",
|
|
43
|
+
"config": {
|
|
44
|
+
"candidate": dict(candidate),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"on_done": "reset",
|
|
48
|
+
"safety": {"max_time_s": 300},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def default_rollout_score_extractor(response_payload: dict[str, Any]) -> float:
|
|
53
|
+
"""Extract the rollout reward from a Synth Container response payload."""
|
|
54
|
+
reward_info = response_payload.get("metrics")
|
|
55
|
+
if not isinstance(reward_info, dict):
|
|
56
|
+
reward_info = response_payload.get("reward_info")
|
|
57
|
+
if not isinstance(reward_info, dict):
|
|
58
|
+
raise ValueError("rollout response is missing metrics/reward_info")
|
|
59
|
+
reward = reward_info.get("outcome_reward")
|
|
60
|
+
if reward is None:
|
|
61
|
+
raise ValueError("rollout response metrics are missing outcome_reward")
|
|
62
|
+
return float(reward)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ContainerEvaluationBatch:
|
|
67
|
+
"""Batch output shaped to GEPA adapter expectations."""
|
|
68
|
+
|
|
69
|
+
outputs: list[dict[str, Any]]
|
|
70
|
+
scores: list[float]
|
|
71
|
+
trajectories: list[dict[str, Any] | None] | None = None
|
|
72
|
+
objective_scores: list[dict[str, float]] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SynthContainerLearningAdapter:
|
|
76
|
+
"""Evaluate candidates against a Synth-compatible Container rollout endpoint."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
container_url: str,
|
|
82
|
+
request_builder: RolloutRequestBuilder,
|
|
83
|
+
api_key: str | None = None,
|
|
84
|
+
headers: Mapping[str, str] | None = None,
|
|
85
|
+
timeout_seconds: float = 30.0,
|
|
86
|
+
score_extractor: RolloutScoreExtractor = default_rollout_score_extractor,
|
|
87
|
+
) -> None:
|
|
88
|
+
self._container_url = container_url.rstrip("/")
|
|
89
|
+
self._request_builder = request_builder
|
|
90
|
+
self._api_key = api_key
|
|
91
|
+
self._headers = dict(headers or {})
|
|
92
|
+
self._timeout_seconds = float(timeout_seconds)
|
|
93
|
+
self._score_extractor = score_extractor
|
|
94
|
+
|
|
95
|
+
def _post_rollout(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
body = json.dumps(payload).encode("utf-8")
|
|
97
|
+
headers = {"content-type": "application/json", **self._headers}
|
|
98
|
+
if self._api_key:
|
|
99
|
+
headers["x-api-key"] = self._api_key
|
|
100
|
+
http_request = request.Request(
|
|
101
|
+
f"{self._container_url}/rollout",
|
|
102
|
+
data=body,
|
|
103
|
+
headers=headers,
|
|
104
|
+
method="POST",
|
|
105
|
+
)
|
|
106
|
+
with request.urlopen(http_request, timeout=self._timeout_seconds) as response:
|
|
107
|
+
return json.loads(response.read().decode("utf-8"))
|
|
108
|
+
|
|
109
|
+
def evaluate(
|
|
110
|
+
self,
|
|
111
|
+
batch: list[Mapping[str, Any]],
|
|
112
|
+
candidate: dict[str, str],
|
|
113
|
+
capture_traces: bool = False,
|
|
114
|
+
) -> ContainerEvaluationBatch:
|
|
115
|
+
outputs: list[dict[str, Any]] = []
|
|
116
|
+
scores: list[float] = []
|
|
117
|
+
trajectories: list[dict[str, Any] | None] = []
|
|
118
|
+
objective_scores: list[dict[str, float]] = []
|
|
119
|
+
|
|
120
|
+
for index, example in enumerate(batch):
|
|
121
|
+
correlation_id = f"prompt-opt-{index}-{uuid.uuid4().hex}"
|
|
122
|
+
rollout_request = self._request_builder(example, candidate, correlation_id)
|
|
123
|
+
rollout_response = self._post_rollout(rollout_request)
|
|
124
|
+
score = self._score_extractor(rollout_response)
|
|
125
|
+
reward_info = rollout_response.get("metrics") or rollout_response.get("reward_info") or {}
|
|
126
|
+
outcome_objectives = reward_info.get("outcome_objectives")
|
|
127
|
+
outputs.append(
|
|
128
|
+
{
|
|
129
|
+
"request": rollout_request,
|
|
130
|
+
"response": rollout_response,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
scores.append(score)
|
|
134
|
+
trajectories.append(rollout_response.get("trace") if capture_traces else None)
|
|
135
|
+
objective_scores.append(
|
|
136
|
+
dict(outcome_objectives) if isinstance(outcome_objectives, dict) else {"reward": score}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return ContainerEvaluationBatch(
|
|
140
|
+
outputs=outputs,
|
|
141
|
+
scores=scores,
|
|
142
|
+
trajectories=trajectories,
|
|
143
|
+
objective_scores=objective_scores,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def make_reflective_dataset(
|
|
147
|
+
self,
|
|
148
|
+
candidate: dict[str, str],
|
|
149
|
+
eval_batch: ContainerEvaluationBatch,
|
|
150
|
+
components_to_update: list[str],
|
|
151
|
+
) -> Mapping[str, Sequence[Mapping[str, Any]]]:
|
|
152
|
+
reflective_rows: list[dict[str, Any]] = []
|
|
153
|
+
for idx, (output, score) in enumerate(zip(eval_batch.outputs, eval_batch.scores)):
|
|
154
|
+
reflective_rows.append(
|
|
155
|
+
{
|
|
156
|
+
"index": idx,
|
|
157
|
+
"candidate": dict(candidate),
|
|
158
|
+
"score": float(score),
|
|
159
|
+
"output": output,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return {component: tuple(reflective_rows) for component in components_to_update}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = [
|
|
166
|
+
"ContainerEvaluationBatch",
|
|
167
|
+
"SynthContainerLearningAdapter",
|
|
168
|
+
"default_rollout_request_builder",
|
|
169
|
+
"default_rollout_score_extractor",
|
|
170
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Synth-compatible offline learning adapters for local use."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ScoreFunction = Callable[[Mapping[str, Any], dict[str, str]], float]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class LocalEvaluator:
|
|
15
|
+
"""Evaluates one dataset example against one candidate."""
|
|
16
|
+
|
|
17
|
+
score_fn: ScoreFunction
|
|
18
|
+
|
|
19
|
+
def score_batch(self, batch: Sequence[Mapping[str, Any]], candidate: dict[str, str]) -> list[float]:
|
|
20
|
+
"""Return per-example scores for the candidate."""
|
|
21
|
+
return [float(self.score_fn(example, candidate)) for example in batch]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class LocalEvaluationBatch:
|
|
26
|
+
"""Local batch output shaped to GEPA adapter expectations."""
|
|
27
|
+
|
|
28
|
+
outputs: list[dict[str, Any]]
|
|
29
|
+
scores: list[float]
|
|
30
|
+
trajectories: list[dict[str, Any]] | None = None
|
|
31
|
+
objective_scores: list[dict[str, float]] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SynthOfflineLearningAdapter:
|
|
35
|
+
"""Local adapter with a GEPA-compatible evaluation shape.
|
|
36
|
+
|
|
37
|
+
This adapter is intentionally lightweight so it can run completely offline
|
|
38
|
+
without backend dependencies.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, evaluator: LocalEvaluator) -> None:
|
|
42
|
+
self._evaluator = evaluator
|
|
43
|
+
|
|
44
|
+
def evaluate(
|
|
45
|
+
self,
|
|
46
|
+
batch: list[Mapping[str, Any]],
|
|
47
|
+
candidate: dict[str, str],
|
|
48
|
+
capture_traces: bool = False,
|
|
49
|
+
) -> LocalEvaluationBatch:
|
|
50
|
+
del capture_traces
|
|
51
|
+
scores = self._evaluator.score_batch(batch, candidate)
|
|
52
|
+
outputs = [{"candidate": candidate, "score": score} for score in scores]
|
|
53
|
+
trajectories = [{"kind": "offline_local"} for _ in scores]
|
|
54
|
+
objective_scores = [{"reward": score} for score in scores]
|
|
55
|
+
return LocalEvaluationBatch(
|
|
56
|
+
outputs=outputs,
|
|
57
|
+
scores=scores,
|
|
58
|
+
trajectories=trajectories,
|
|
59
|
+
objective_scores=objective_scores,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def make_reflective_dataset(
|
|
63
|
+
self,
|
|
64
|
+
candidate: dict[str, str],
|
|
65
|
+
eval_batch: LocalEvaluationBatch,
|
|
66
|
+
components_to_update: list[str],
|
|
67
|
+
) -> Mapping[str, Sequence[Mapping[str, Any]]]:
|
|
68
|
+
reflective_rows: list[dict[str, Any]] = []
|
|
69
|
+
for idx, (output, score) in enumerate(zip(eval_batch.outputs, eval_batch.scores)):
|
|
70
|
+
reflective_rows.append(
|
|
71
|
+
{
|
|
72
|
+
"index": idx,
|
|
73
|
+
"candidate": dict(candidate),
|
|
74
|
+
"score": float(score),
|
|
75
|
+
"output": output,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
return {
|
|
79
|
+
component: tuple(reflective_rows)
|
|
80
|
+
for component in components_to_update
|
|
81
|
+
}
|
prompt_opt/dspy/gepa.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""DSPy-compatible MIPROv2 wrapper backed by the local offline SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from prompt_opt.mipro import proposer_backends
|
|
11
|
+
from prompt_opt.sdk.optimization.internal.prompt_learning import PromptLearningJob
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_text_field(example: Any, keys: tuple[str, ...]) -> str:
|
|
15
|
+
if isinstance(example, dict):
|
|
16
|
+
for key in keys:
|
|
17
|
+
value = example.get(key)
|
|
18
|
+
if value is not None:
|
|
19
|
+
return str(value)
|
|
20
|
+
for key in keys:
|
|
21
|
+
if hasattr(example, key):
|
|
22
|
+
value = getattr(example, key)
|
|
23
|
+
if value is not None:
|
|
24
|
+
return str(value)
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _materialize_dataset(dataset: list[Any]) -> list[dict[str, Any]]:
|
|
29
|
+
rows: list[dict[str, Any]] = []
|
|
30
|
+
for item in dataset:
|
|
31
|
+
input_text = _extract_text_field(item, ("input", "question", "query", "text"))
|
|
32
|
+
answer_text = _extract_text_field(item, ("answer", "label", "expected", "output"))
|
|
33
|
+
additional_context: dict[str, Any] = {}
|
|
34
|
+
if isinstance(item, dict):
|
|
35
|
+
raw_context = item.get("additional_context")
|
|
36
|
+
if isinstance(raw_context, dict):
|
|
37
|
+
additional_context = dict(raw_context)
|
|
38
|
+
labels_blob = str(additional_context.get("labels", "")).strip()
|
|
39
|
+
if labels_blob:
|
|
40
|
+
input_text = f"{input_text}\n\nLabels: {labels_blob}"
|
|
41
|
+
rows.append({"input": input_text, "answer": answer_text, "metadata": {}})
|
|
42
|
+
return rows
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_num_trials(
|
|
46
|
+
*,
|
|
47
|
+
auto: Literal["light", "medium", "heavy"] | None,
|
|
48
|
+
explicit_trials: int | None,
|
|
49
|
+
num_candidates: int | None,
|
|
50
|
+
student: Any,
|
|
51
|
+
) -> int:
|
|
52
|
+
if explicit_trials is not None:
|
|
53
|
+
return max(1, int(explicit_trials))
|
|
54
|
+
if auto is None and num_candidates is not None:
|
|
55
|
+
predictor_count = 1
|
|
56
|
+
named_predictors = getattr(student, "named_predictors", None)
|
|
57
|
+
if callable(named_predictors):
|
|
58
|
+
try:
|
|
59
|
+
predictor_count = max(1, len(list(named_predictors())))
|
|
60
|
+
except Exception:
|
|
61
|
+
predictor_count = 1
|
|
62
|
+
suggested = int(max(2 * (predictor_count * 2) * math.log2(max(num_candidates, 2)), 1.5 * num_candidates))
|
|
63
|
+
return max(1, suggested)
|
|
64
|
+
if auto == "light":
|
|
65
|
+
return 7
|
|
66
|
+
if auto == "medium":
|
|
67
|
+
return 20
|
|
68
|
+
if auto == "heavy":
|
|
69
|
+
return 50
|
|
70
|
+
raise ValueError("num_trials must be provided when auto is None.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_seed_candidate_from_student(student: Any) -> dict[str, str]:
|
|
74
|
+
named_predictors = getattr(student, "named_predictors", None)
|
|
75
|
+
if callable(named_predictors):
|
|
76
|
+
try:
|
|
77
|
+
entries = list(named_predictors())
|
|
78
|
+
candidate: dict[str, str] = {}
|
|
79
|
+
for entry_index, (name, predictor) in enumerate(entries):
|
|
80
|
+
signature = getattr(predictor, "signature", None)
|
|
81
|
+
instructions = getattr(signature, "instructions", None)
|
|
82
|
+
if isinstance(instructions, str) and instructions.strip():
|
|
83
|
+
candidate[str(name or f"predictor_{entry_index}")] = instructions.strip()
|
|
84
|
+
if candidate:
|
|
85
|
+
return candidate
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
return {"default": "You are a helpful assistant."}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _apply_prompts_to_student(student: Any, prompts: dict[str, str]) -> Any:
|
|
92
|
+
named_predictors = getattr(student, "named_predictors", None)
|
|
93
|
+
if not callable(named_predictors):
|
|
94
|
+
return student
|
|
95
|
+
try:
|
|
96
|
+
for name, predictor in list(named_predictors()):
|
|
97
|
+
prompt = prompts.get(str(name))
|
|
98
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
99
|
+
continue
|
|
100
|
+
signature = getattr(predictor, "signature", None)
|
|
101
|
+
if signature is None:
|
|
102
|
+
continue
|
|
103
|
+
with_instructions = getattr(signature, "with_instructions", None)
|
|
104
|
+
if callable(with_instructions):
|
|
105
|
+
predictor.signature = with_instructions(prompt)
|
|
106
|
+
else:
|
|
107
|
+
signature.instructions = prompt
|
|
108
|
+
except Exception:
|
|
109
|
+
return student
|
|
110
|
+
return student
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_best_prompts(result_payload: dict[str, Any]) -> dict[str, str]:
|
|
114
|
+
best_candidate = result_payload.get("best_candidate")
|
|
115
|
+
if not isinstance(best_candidate, dict):
|
|
116
|
+
return {}
|
|
117
|
+
stage_items = best_candidate.get("stages") or best_candidate.get("candidate", {}).get("stages") or []
|
|
118
|
+
prompts: dict[str, str] = {}
|
|
119
|
+
for stage in stage_items:
|
|
120
|
+
if not isinstance(stage, dict):
|
|
121
|
+
continue
|
|
122
|
+
stage_key = str(stage.get("id") or stage.get("name") or f"stage_{len(prompts)}")
|
|
123
|
+
for message in stage.get("messages", []):
|
|
124
|
+
if isinstance(message, dict) and message.get("role") == "system":
|
|
125
|
+
text = message.get("pattern") or message.get("content")
|
|
126
|
+
if isinstance(text, str) and text.strip():
|
|
127
|
+
prompts[stage_key] = text.strip()
|
|
128
|
+
break
|
|
129
|
+
return prompts
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class MIPROv2:
|
|
133
|
+
"""Drop-in replacement for `dspy.MIPROv2` running locally only."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
metric: Any,
|
|
138
|
+
prompt_model: Any | None = None,
|
|
139
|
+
task_model: Any | None = None,
|
|
140
|
+
teacher_settings: dict[str, Any] | None = None,
|
|
141
|
+
max_bootstrapped_demos: int = 4,
|
|
142
|
+
max_labeled_demos: int = 4,
|
|
143
|
+
auto: Literal["light", "medium", "heavy"] | None = "light",
|
|
144
|
+
num_candidates: int | None = None,
|
|
145
|
+
num_threads: int | None = None,
|
|
146
|
+
max_errors: int | None = None,
|
|
147
|
+
seed: int = 9,
|
|
148
|
+
init_temperature: float = 1.0,
|
|
149
|
+
verbose: bool = False,
|
|
150
|
+
track_stats: bool = True,
|
|
151
|
+
log_dir: str | None = None,
|
|
152
|
+
metric_threshold: float | None = None,
|
|
153
|
+
backend_mode: Literal["local"] = "local",
|
|
154
|
+
proposer_backend: Literal["single_prompt", "rlm"] = "single_prompt",
|
|
155
|
+
**kwargs: Any,
|
|
156
|
+
) -> None:
|
|
157
|
+
del metric, teacher_settings, num_threads, max_errors, init_temperature, log_dir, metric_threshold, kwargs
|
|
158
|
+
if proposer_backend not in proposer_backends():
|
|
159
|
+
raise ValueError(f"Unsupported proposer_backend={proposer_backend!r}")
|
|
160
|
+
self.prompt_model = prompt_model
|
|
161
|
+
self.task_model = task_model
|
|
162
|
+
self.max_bootstrapped_demos = max_bootstrapped_demos
|
|
163
|
+
self.max_labeled_demos = max_labeled_demos
|
|
164
|
+
self.auto = auto
|
|
165
|
+
self.num_candidates = num_candidates
|
|
166
|
+
self.seed = seed
|
|
167
|
+
self.verbose = verbose
|
|
168
|
+
self.track_stats = track_stats
|
|
169
|
+
self.backend_mode = backend_mode
|
|
170
|
+
self.proposer_backend = proposer_backend
|
|
171
|
+
|
|
172
|
+
def compile(
|
|
173
|
+
self,
|
|
174
|
+
student: Any,
|
|
175
|
+
*,
|
|
176
|
+
trainset: list[Any],
|
|
177
|
+
teacher: Any = None,
|
|
178
|
+
valset: list[Any] | None = None,
|
|
179
|
+
num_trials: int | None = None,
|
|
180
|
+
num_candidates: int | None = None,
|
|
181
|
+
max_bootstrapped_demos: int | None = None,
|
|
182
|
+
max_labeled_demos: int | None = None,
|
|
183
|
+
seed: int | None = None,
|
|
184
|
+
minibatch: bool = True,
|
|
185
|
+
minibatch_size: int = 35,
|
|
186
|
+
minibatch_full_eval_steps: int = 5,
|
|
187
|
+
program_aware_proposer: bool = True,
|
|
188
|
+
data_aware_proposer: bool = True,
|
|
189
|
+
view_data_batch_size: int = 10,
|
|
190
|
+
tip_aware_proposer: bool = True,
|
|
191
|
+
fewshot_aware_proposer: bool = True,
|
|
192
|
+
requires_permission_to_run: bool | None = None,
|
|
193
|
+
provide_traceback: bool | None = None,
|
|
194
|
+
**kwargs: Any,
|
|
195
|
+
) -> Any:
|
|
196
|
+
del (
|
|
197
|
+
teacher,
|
|
198
|
+
max_bootstrapped_demos,
|
|
199
|
+
max_labeled_demos,
|
|
200
|
+
minibatch,
|
|
201
|
+
minibatch_size,
|
|
202
|
+
minibatch_full_eval_steps,
|
|
203
|
+
program_aware_proposer,
|
|
204
|
+
data_aware_proposer,
|
|
205
|
+
view_data_batch_size,
|
|
206
|
+
tip_aware_proposer,
|
|
207
|
+
fewshot_aware_proposer,
|
|
208
|
+
requires_permission_to_run,
|
|
209
|
+
provide_traceback,
|
|
210
|
+
)
|
|
211
|
+
if self.backend_mode != "local":
|
|
212
|
+
raise ValueError("prompt-opt is local-only. Use backend_mode='local'.")
|
|
213
|
+
|
|
214
|
+
if not callable(getattr(self.task_model, "__call__", None)):
|
|
215
|
+
raise ValueError(
|
|
216
|
+
"Local backend_mode requires task_model to be callable(prompt)->str. "
|
|
217
|
+
"Model-id strings are not supported in local-only mode."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
run_seed = int(seed if seed is not None else self.seed)
|
|
221
|
+
train_records = _materialize_dataset(trainset)
|
|
222
|
+
val_records = _materialize_dataset(valset) if valset is not None else train_records
|
|
223
|
+
effective_num_candidates = int(num_candidates) if num_candidates is not None else self.num_candidates
|
|
224
|
+
trials = _resolve_num_trials(
|
|
225
|
+
auto=self.auto,
|
|
226
|
+
explicit_trials=num_trials,
|
|
227
|
+
num_candidates=effective_num_candidates,
|
|
228
|
+
student=student,
|
|
229
|
+
)
|
|
230
|
+
seed_candidate = _extract_seed_candidate_from_student(student)
|
|
231
|
+
stages = [
|
|
232
|
+
{
|
|
233
|
+
"id": stage_id,
|
|
234
|
+
"name": stage_id,
|
|
235
|
+
"messages": [
|
|
236
|
+
{"role": "system", "pattern": prompt, "order": 0},
|
|
237
|
+
{"role": "user", "pattern": "{input}", "order": 1},
|
|
238
|
+
],
|
|
239
|
+
"wildcards": {},
|
|
240
|
+
}
|
|
241
|
+
for stage_id, prompt in seed_candidate.items()
|
|
242
|
+
]
|
|
243
|
+
prompt_learning_config = {
|
|
244
|
+
"prompt_learning": {
|
|
245
|
+
"algorithm": "mipro",
|
|
246
|
+
"execution_mode": "retrieved",
|
|
247
|
+
"task_data": {
|
|
248
|
+
"train_examples": train_records,
|
|
249
|
+
"validation_examples": val_records,
|
|
250
|
+
},
|
|
251
|
+
"mipro": {
|
|
252
|
+
"initial_candidate": {"stages": stages},
|
|
253
|
+
"num_candidates": max(1, int(effective_num_candidates or 8)),
|
|
254
|
+
"max_iterations": max(1, int(trials)),
|
|
255
|
+
"early_stop_rounds": 3,
|
|
256
|
+
"min_improvement": 1e-6,
|
|
257
|
+
"seed": run_seed,
|
|
258
|
+
"proposer_backend": self.proposer_backend,
|
|
259
|
+
"termination_conditions": {
|
|
260
|
+
"total_rollouts": max(1, int(trials)) * max(1, len(val_records)),
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
"local_runtime": {
|
|
264
|
+
"task_model": self.task_model,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
job = PromptLearningJob.from_dict(
|
|
269
|
+
prompt_learning_config,
|
|
270
|
+
backend_url="local://prompt-opt",
|
|
271
|
+
api_key="local",
|
|
272
|
+
)
|
|
273
|
+
job.submit()
|
|
274
|
+
result = job.stream_until_complete(timeout=300.0, interval=0.05).to_dict()
|
|
275
|
+
optimized_program = copy.deepcopy(student)
|
|
276
|
+
best_prompts = _extract_best_prompts(result)
|
|
277
|
+
if best_prompts:
|
|
278
|
+
optimized_program = _apply_prompts_to_student(optimized_program, best_prompts)
|
|
279
|
+
if self.track_stats:
|
|
280
|
+
setattr(optimized_program, "prompt_opt_result", json.loads(json.dumps(result)))
|
|
281
|
+
return optimized_program
|