orizu 0.3.7 → 0.4.0
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.
- package/README.md +55 -6
- package/dist/banner.js +20 -0
- package/dist/csv.js +117 -9
- package/dist/help.js +109 -10
- package/dist/index.js +736 -44
- package/dist/setup-prompt.js +22 -0
- package/dist/skill-installer.js +243 -17
- package/dist/workspace.js +76 -0
- package/package.json +5 -1
- package/src/banner.ts +21 -0
- package/src/csv.ts +140 -10
- package/src/help.ts +109 -10
- package/src/index.ts +815 -46
- package/src/setup-prompt.ts +27 -0
- package/src/skill-installer.ts +343 -17
- package/src/workspace.ts +112 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/__init__.py +2 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +57 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +37 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +233 -17
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +98 -17
- package/vendor/skills/orizu-cli/SKILL.md +8 -3
- package/vendor/skills/orizu-cli/references/cli-reference.md +25 -4
- package/vendor/skills/orizu-cli/references/optimization-reports.md +2 -2
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +17 -5
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -11,6 +12,8 @@ from .optimizer import TextGepaConfig, optimize_loaded_text_candidate
|
|
|
11
12
|
from .reflection import reflect_with_provider
|
|
12
13
|
from .runner import make_candidate_runner, make_scorer_runner
|
|
13
14
|
|
|
15
|
+
BUDGET_OPTION_FLAGS = ("--budget", "--max-metric-calls", "--max-full-evals", "--max-iterations")
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
def positive_int(value: str) -> int:
|
|
16
19
|
parsed = int(value)
|
|
@@ -46,6 +49,38 @@ def read_json_object_arg(value: str | None, flag_name: str) -> dict[str, Any] |
|
|
|
46
49
|
return parsed
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
def _flag_occurrences(raw_args: list[str], flag: str) -> int:
|
|
53
|
+
return sum(1 for arg in raw_args if arg == flag or arg.startswith(f"{flag}="))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_budget_option(flag: str, count: int) -> str:
|
|
57
|
+
return flag if count == 1 else f"{flag} ({count} times)"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def apply_budget_defaults(args: argparse.Namespace, raw_args: list[str]) -> None:
|
|
61
|
+
occurrences = {
|
|
62
|
+
flag: _flag_occurrences(raw_args, flag)
|
|
63
|
+
for flag in BUDGET_OPTION_FLAGS
|
|
64
|
+
}
|
|
65
|
+
provided = [
|
|
66
|
+
_format_budget_option(flag, count)
|
|
67
|
+
for flag, count in occurrences.items()
|
|
68
|
+
if count > 0
|
|
69
|
+
]
|
|
70
|
+
total = sum(occurrences.values())
|
|
71
|
+
|
|
72
|
+
if total > 1:
|
|
73
|
+
allowed = ", ".join(BUDGET_OPTION_FLAGS)
|
|
74
|
+
received = ", ".join(provided)
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Budget options are mutually exclusive; choose at most one of {allowed}. "
|
|
77
|
+
f"Received: {received}."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if total == 0:
|
|
81
|
+
args.budget = TextGepaConfig.budget
|
|
82
|
+
|
|
83
|
+
|
|
49
84
|
def main() -> None:
|
|
50
85
|
parser = argparse.ArgumentParser(prog="orizu-gepa")
|
|
51
86
|
parser.add_argument("--project", required=True)
|
|
@@ -60,8 +95,8 @@ def main() -> None:
|
|
|
60
95
|
parser.add_argument("--split-set-id", required=True)
|
|
61
96
|
parser.add_argument("--train-split", default="train")
|
|
62
97
|
parser.add_argument("--val-split", default="validation")
|
|
63
|
-
parser.add_argument("--budget",
|
|
64
|
-
parser.add_argument("--max-iterations", type=positive_int
|
|
98
|
+
parser.add_argument("--budget", choices=["auto", "light", "medium", "heavy"])
|
|
99
|
+
parser.add_argument("--max-iterations", type=positive_int)
|
|
65
100
|
parser.add_argument("--minibatch-size", type=positive_int, default=3)
|
|
66
101
|
parser.add_argument("--num-threads", type=num_threads_arg, default=TextGepaConfig.num_threads)
|
|
67
102
|
parser.add_argument(
|
|
@@ -76,6 +111,18 @@ def main() -> None:
|
|
|
76
111
|
parser.add_argument("--reflection-model", default=TextGepaConfig.reflection_model)
|
|
77
112
|
parser.add_argument("--reflection-temperature", type=float)
|
|
78
113
|
parser.add_argument("--reflection-max-tokens", type=positive_int)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--reflection-retry-attempts",
|
|
116
|
+
type=positive_int,
|
|
117
|
+
default=TextGepaConfig.reflection_retry_attempts,
|
|
118
|
+
help="Total attempts for retryable reflection HTTP failures.",
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--reflection-http-timeout-seconds",
|
|
122
|
+
type=positive_int,
|
|
123
|
+
default=TextGepaConfig.reflection_http_timeout_seconds,
|
|
124
|
+
help="Per-attempt timeout for reflection HTTP calls.",
|
|
125
|
+
)
|
|
79
126
|
parser.add_argument("--reflection-prompt-template")
|
|
80
127
|
parser.add_argument("--reflection-provider-settings")
|
|
81
128
|
parser.add_argument(
|
|
@@ -99,7 +146,12 @@ def main() -> None:
|
|
|
99
146
|
parser.add_argument("--log-dir", default="logs")
|
|
100
147
|
parser.add_argument("--no-local-log", action="store_true")
|
|
101
148
|
parser.add_argument("--metadata", default="{}")
|
|
102
|
-
|
|
149
|
+
raw_args = sys.argv[1:]
|
|
150
|
+
args = parser.parse_args(raw_args)
|
|
151
|
+
try:
|
|
152
|
+
apply_budget_defaults(args, raw_args)
|
|
153
|
+
except ValueError as error:
|
|
154
|
+
parser.error(str(error))
|
|
103
155
|
|
|
104
156
|
metadata = json.loads(args.metadata)
|
|
105
157
|
if not isinstance(metadata, dict):
|
|
@@ -117,6 +169,8 @@ def main() -> None:
|
|
|
117
169
|
reflection_model=args.reflection_model,
|
|
118
170
|
reflection_temperature=args.reflection_temperature,
|
|
119
171
|
reflection_max_tokens=args.reflection_max_tokens,
|
|
172
|
+
reflection_retry_attempts=args.reflection_retry_attempts,
|
|
173
|
+
reflection_http_timeout_seconds=args.reflection_http_timeout_seconds,
|
|
120
174
|
reflection_prompt_template=read_text_arg(args.reflection_prompt_template),
|
|
121
175
|
reflection_provider_settings=read_json_object_arg(args.reflection_provider_settings, "--reflection-provider-settings") or {},
|
|
122
176
|
skip_perfect_parent_reflection=args.skip_perfect_parent_reflection,
|
|
@@ -12,6 +12,39 @@ from typing import Any
|
|
|
12
12
|
from .optimizer import DatasetRow, PromptContext
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _format_number(value: Any) -> str | None:
|
|
16
|
+
if isinstance(value, bool):
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(value, int):
|
|
19
|
+
return str(value)
|
|
20
|
+
if isinstance(value, float):
|
|
21
|
+
return f"{value:g}"
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _progress_log_suffix(event_type: str, payload: dict[str, Any] | None) -> str:
|
|
26
|
+
if event_type != "optimization_progress" or not isinstance(payload, dict):
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
percent_value = payload.get("percent")
|
|
30
|
+
if percent_value is None:
|
|
31
|
+
percent_value = payload.get("progress_percent")
|
|
32
|
+
percent = _format_number(percent_value)
|
|
33
|
+
metric_calls_remaining = _format_number(payload.get("metric_calls_remaining"))
|
|
34
|
+
metric_call_budget_value = payload.get("metric_call_budget")
|
|
35
|
+
if metric_call_budget_value is None:
|
|
36
|
+
metric_call_budget_value = payload.get("approx_metric_call_budget")
|
|
37
|
+
metric_call_budget = _format_number(metric_call_budget_value)
|
|
38
|
+
parts: list[str] = []
|
|
39
|
+
if percent is not None:
|
|
40
|
+
parts.append(f"{percent}%")
|
|
41
|
+
if metric_calls_remaining is not None:
|
|
42
|
+
budget_suffix = f" / {metric_call_budget}" if metric_call_budget is not None else ""
|
|
43
|
+
parts.append(f"{metric_calls_remaining}{budget_suffix} metric calls left")
|
|
44
|
+
|
|
45
|
+
return f" {'; '.join(parts)}" if parts else ""
|
|
46
|
+
|
|
47
|
+
|
|
15
48
|
@dataclass(frozen=True)
|
|
16
49
|
class OrizuClient:
|
|
17
50
|
api_url: str
|
|
@@ -306,7 +339,10 @@ class OrizuEventSink:
|
|
|
306
339
|
parent_candidate_id=parent_candidate_id,
|
|
307
340
|
child_candidate_id=child_candidate_id,
|
|
308
341
|
)
|
|
309
|
-
print(
|
|
342
|
+
print(
|
|
343
|
+
f"[orizu-gepa] {self.sequence:03d} {event_type}{_progress_log_suffix(event_type, payload)}",
|
|
344
|
+
flush=True,
|
|
345
|
+
)
|
|
310
346
|
return
|
|
311
347
|
except Exception as exc:
|
|
312
348
|
last_error = exc
|
|
@@ -44,7 +44,7 @@ Return only the complete updated parameter text, exactly as it should be used by
|
|
|
44
44
|
Do not include analysis, explanation, labels, XML tags, or a surrounding markdown code fence.
|
|
45
45
|
Your response will be used verbatim as the next candidate."""
|
|
46
46
|
|
|
47
|
-
AUTO_NUM_THREADS_HARD_CAP =
|
|
47
|
+
AUTO_NUM_THREADS_HARD_CAP = 64
|
|
48
48
|
AUTO_NUM_THREADS_WORKER_MEMORY_BYTES = 512 * 1024 * 1024
|
|
49
49
|
AUTO_NUM_THREADS_MEMORY_RESERVE_BYTES = 1024 * 1024 * 1024
|
|
50
50
|
AUTO_NUM_THREADS_FD_HEADROOM = 64
|
|
@@ -142,6 +142,10 @@ class ReflectionResult:
|
|
|
142
142
|
candidate_text: str
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
class RetryableReflectionError(RuntimeError):
|
|
146
|
+
"""Raised when a transient reflection-provider failure remains after retries."""
|
|
147
|
+
|
|
148
|
+
|
|
145
149
|
def _dspy_auto_metric_budget(
|
|
146
150
|
*,
|
|
147
151
|
num_components: int,
|
|
@@ -170,13 +174,37 @@ def _dspy_auto_metric_budget(
|
|
|
170
174
|
return total
|
|
171
175
|
|
|
172
176
|
|
|
177
|
+
def _approx_metric_calls_for_candidate_budget(
|
|
178
|
+
*,
|
|
179
|
+
candidate_proposals: int,
|
|
180
|
+
trainset_size: int,
|
|
181
|
+
valset_size: int,
|
|
182
|
+
minibatch_size: int,
|
|
183
|
+
) -> int:
|
|
184
|
+
"""Estimate row-level metric calls for proposal-count budgets."""
|
|
185
|
+
safe_proposals = max(0, candidate_proposals)
|
|
186
|
+
safe_valset_size = max(0, valset_size)
|
|
187
|
+
safe_trainset_size = max(0, trainset_size)
|
|
188
|
+
safe_minibatch_size = max(0, minibatch_size)
|
|
189
|
+
sampled_train_rows = min(safe_trainset_size, safe_minibatch_size)
|
|
190
|
+
per_iteration = (2 * sampled_train_rows) + safe_valset_size
|
|
191
|
+
return safe_valset_size + (safe_proposals * per_iteration)
|
|
192
|
+
|
|
193
|
+
|
|
173
194
|
@dataclass
|
|
174
195
|
class Budget:
|
|
175
196
|
budget_kind: str
|
|
176
197
|
limit: int
|
|
198
|
+
approx_metric_call_limit: int | None = None
|
|
177
199
|
used_metric_calls: int = 0
|
|
200
|
+
used_reflection_failure_metric_charges: int = 0
|
|
178
201
|
used_full_evals: int = 0
|
|
179
202
|
used_candidate_proposals: int = 0
|
|
203
|
+
used_iterations: int = 0
|
|
204
|
+
|
|
205
|
+
def __post_init__(self) -> None:
|
|
206
|
+
if self.approx_metric_call_limit is None and self.budget_kind == "max_metric_calls":
|
|
207
|
+
self.approx_metric_call_limit = self.limit
|
|
180
208
|
|
|
181
209
|
@classmethod
|
|
182
210
|
def from_config(
|
|
@@ -188,11 +216,33 @@ class Budget:
|
|
|
188
216
|
num_components: int = 1,
|
|
189
217
|
) -> "Budget":
|
|
190
218
|
if config.max_metric_calls is not None:
|
|
191
|
-
return cls(
|
|
219
|
+
return cls(
|
|
220
|
+
"max_metric_calls",
|
|
221
|
+
config.max_metric_calls,
|
|
222
|
+
approx_metric_call_limit=config.max_metric_calls,
|
|
223
|
+
)
|
|
192
224
|
if config.max_full_evals is not None:
|
|
193
|
-
|
|
225
|
+
metric_limit = config.max_full_evals * (trainset_size + valset_size)
|
|
226
|
+
return cls(
|
|
227
|
+
"max_metric_calls",
|
|
228
|
+
metric_limit,
|
|
229
|
+
approx_metric_call_limit=metric_limit,
|
|
230
|
+
)
|
|
194
231
|
if config.max_candidate_proposals is not None:
|
|
195
|
-
return cls(
|
|
232
|
+
return cls(
|
|
233
|
+
"max_candidate_proposals",
|
|
234
|
+
config.max_candidate_proposals,
|
|
235
|
+
approx_metric_call_limit=_approx_metric_calls_for_candidate_budget(
|
|
236
|
+
candidate_proposals=config.max_candidate_proposals,
|
|
237
|
+
trainset_size=trainset_size,
|
|
238
|
+
valset_size=valset_size,
|
|
239
|
+
minibatch_size=config.minibatch_size,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
if config.max_iterations is not None:
|
|
243
|
+
return cls("max_iterations", config.max_iterations)
|
|
244
|
+
if not config.budget:
|
|
245
|
+
raise ValueError("No optimization budget configured.")
|
|
196
246
|
preset = "medium" if config.budget == "auto" else config.budget
|
|
197
247
|
preset_candidates = {
|
|
198
248
|
"light": 6,
|
|
@@ -200,46 +250,106 @@ class Budget:
|
|
|
200
250
|
"heavy": 18,
|
|
201
251
|
"high": 18,
|
|
202
252
|
}.get(preset, 6)
|
|
253
|
+
metric_limit = _dspy_auto_metric_budget(
|
|
254
|
+
num_components=num_components,
|
|
255
|
+
num_candidates=preset_candidates,
|
|
256
|
+
valset_size=valset_size,
|
|
257
|
+
)
|
|
203
258
|
return cls(
|
|
204
259
|
"max_metric_calls",
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
num_candidates=preset_candidates,
|
|
208
|
-
valset_size=valset_size,
|
|
209
|
-
),
|
|
260
|
+
metric_limit,
|
|
261
|
+
approx_metric_call_limit=metric_limit,
|
|
210
262
|
)
|
|
211
263
|
|
|
264
|
+
@property
|
|
265
|
+
def metric_budget_used(self) -> int:
|
|
266
|
+
return self.used_metric_calls + self.used_reflection_failure_metric_charges
|
|
267
|
+
|
|
212
268
|
@property
|
|
213
269
|
def used(self) -> int:
|
|
214
270
|
if self.budget_kind == "max_full_evals":
|
|
215
271
|
return self.used_full_evals
|
|
216
272
|
if self.budget_kind == "max_candidate_proposals":
|
|
217
273
|
return self.used_candidate_proposals
|
|
218
|
-
|
|
274
|
+
if self.budget_kind == "max_iterations":
|
|
275
|
+
return self.used_iterations
|
|
276
|
+
return self.metric_budget_used
|
|
219
277
|
|
|
220
278
|
@property
|
|
221
279
|
def remaining(self) -> int:
|
|
222
280
|
return max(0, self.limit - self.used)
|
|
223
281
|
|
|
282
|
+
@property
|
|
283
|
+
def metric_calls_remaining(self) -> int:
|
|
284
|
+
if self.approx_metric_call_limit is None:
|
|
285
|
+
return 0
|
|
286
|
+
if self.remaining == 0:
|
|
287
|
+
return 0
|
|
288
|
+
return max(0, self.approx_metric_call_limit - self.metric_budget_used)
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def progress_percent(self) -> float:
|
|
292
|
+
if self.remaining == 0:
|
|
293
|
+
return 100.0
|
|
294
|
+
if self.approx_metric_call_limit is None:
|
|
295
|
+
if self.limit <= 0:
|
|
296
|
+
return 100.0 if self.used > 0 else 0.0
|
|
297
|
+
return min(100.0, max(0.0, (self.used / self.limit) * 100.0))
|
|
298
|
+
if self.approx_metric_call_limit <= 0:
|
|
299
|
+
# A zero-sized budget with any recorded metric work should still read as complete.
|
|
300
|
+
return 100.0 if self.metric_budget_used > 0 else 0.0
|
|
301
|
+
return min(100.0, max(0.0, (self.metric_budget_used / self.approx_metric_call_limit) * 100.0))
|
|
302
|
+
|
|
224
303
|
def allows_iteration(self) -> bool:
|
|
225
304
|
return self.remaining > 0
|
|
226
305
|
|
|
306
|
+
def charge_retryable_reflection_failure(self) -> None:
|
|
307
|
+
if self.budget_kind == "max_metric_calls":
|
|
308
|
+
self.used_reflection_failure_metric_charges += 1
|
|
309
|
+
|
|
227
310
|
def to_payload(self) -> dict[str, Any]:
|
|
228
311
|
return {
|
|
229
312
|
"budget_kind": self.budget_kind,
|
|
230
313
|
"limit": self.limit,
|
|
231
314
|
"used": self.used,
|
|
232
315
|
"remaining": self.remaining,
|
|
316
|
+
"approx_metric_call_limit": self.approx_metric_call_limit,
|
|
317
|
+
"metric_call_budget": self.approx_metric_call_limit,
|
|
318
|
+
"metric_calls_remaining": self.metric_calls_remaining,
|
|
319
|
+
"metric_budget_used": self.metric_budget_used,
|
|
233
320
|
"used_metric_calls": self.used_metric_calls,
|
|
321
|
+
"used_reflection_failure_metric_charges": self.used_reflection_failure_metric_charges,
|
|
234
322
|
"used_full_evals": self.used_full_evals,
|
|
235
323
|
"used_candidate_proposals": self.used_candidate_proposals,
|
|
324
|
+
"used_iterations": self.used_iterations,
|
|
325
|
+
"iteration_budget": self.limit if self.budget_kind == "max_iterations" else None,
|
|
326
|
+
"iterations_remaining": self.remaining if self.budget_kind == "max_iterations" else None,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def progress_payload(self, *, stage: str, iteration: int | None) -> dict[str, Any]:
|
|
330
|
+
percent = round(self.progress_percent, 1)
|
|
331
|
+
return {
|
|
332
|
+
"stage": stage,
|
|
333
|
+
"iteration": iteration,
|
|
334
|
+
"percent": percent,
|
|
335
|
+
"progress_percent": percent,
|
|
336
|
+
"metric_calls_used": self.used_metric_calls,
|
|
337
|
+
"metric_budget_used": self.metric_budget_used,
|
|
338
|
+
"metric_call_budget": self.approx_metric_call_limit,
|
|
339
|
+
"approx_metric_call_budget": self.approx_metric_call_limit,
|
|
340
|
+
"metric_calls_remaining": self.metric_calls_remaining,
|
|
341
|
+
"is_over_budget": (
|
|
342
|
+
self.approx_metric_call_limit is not None and
|
|
343
|
+
self.metric_budget_used > self.approx_metric_call_limit
|
|
344
|
+
),
|
|
345
|
+
"budget": self.to_payload(),
|
|
236
346
|
}
|
|
237
347
|
|
|
238
348
|
|
|
239
349
|
@dataclass(frozen=True)
|
|
240
350
|
class TextGepaConfig:
|
|
241
|
-
budget: str = "
|
|
242
|
-
max_iterations: int =
|
|
351
|
+
budget: str | None = "auto"
|
|
352
|
+
max_iterations: int | None = None
|
|
243
353
|
minibatch_size: int = 3
|
|
244
354
|
num_threads: int | str = "auto"
|
|
245
355
|
candidate_selection_strategy: str = "pareto"
|
|
@@ -250,6 +360,8 @@ class TextGepaConfig:
|
|
|
250
360
|
reflection_model: str = "anthropic/claude-opus-4-7"
|
|
251
361
|
reflection_temperature: float | None = None
|
|
252
362
|
reflection_max_tokens: int | None = None
|
|
363
|
+
reflection_retry_attempts: int = 3
|
|
364
|
+
reflection_http_timeout_seconds: int = 180
|
|
253
365
|
reflection_prompt_template: str | None = None
|
|
254
366
|
reflection_provider_settings: dict[str, Any] = field(default_factory=dict)
|
|
255
367
|
objective: str = "Improve this text candidate to maximize evaluator score while preserving intended behavior."
|
|
@@ -524,7 +636,8 @@ def resolve_num_threads(
|
|
|
524
636
|
parsed = _parse_requested_num_threads(requested)
|
|
525
637
|
row_bound = max(1, max(minibatch_size, validation_count))
|
|
526
638
|
detected_cpu_count = cpu_count if cpu_count is not None else os.cpu_count()
|
|
527
|
-
|
|
639
|
+
normalized_cpu_count = max(1, detected_cpu_count or 1)
|
|
640
|
+
cpu_bound = normalized_cpu_count * 2
|
|
528
641
|
resolved_hard_cap = hard_cap or _positive_int_env(
|
|
529
642
|
"ORIZU_GEPA_AUTO_THREADS_MAX",
|
|
530
643
|
AUTO_NUM_THREADS_HARD_CAP,
|
|
@@ -960,10 +1073,30 @@ def evaluate_candidate(
|
|
|
960
1073
|
]
|
|
961
1074
|
|
|
962
1075
|
|
|
1076
|
+
def _log_iteration_progress(
|
|
1077
|
+
event_sink: EventSink,
|
|
1078
|
+
*,
|
|
1079
|
+
budget: Budget,
|
|
1080
|
+
iteration: int,
|
|
1081
|
+
candidate_id: str | None,
|
|
1082
|
+
) -> None:
|
|
1083
|
+
event_sink.log_event(
|
|
1084
|
+
"optimization_progress",
|
|
1085
|
+
budget.progress_payload(stage="iteration_completed", iteration=iteration),
|
|
1086
|
+
event_layer="extension",
|
|
1087
|
+
iteration=iteration,
|
|
1088
|
+
candidate_id=candidate_id,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
|
|
963
1092
|
def _completed_all_rows(results: list[RowEvaluation], rows: list[DatasetRow]) -> bool:
|
|
964
1093
|
return len(results) == len(rows)
|
|
965
1094
|
|
|
966
1095
|
|
|
1096
|
+
def _has_future_iteration(config: TextGepaConfig, iteration: int) -> bool:
|
|
1097
|
+
return config.max_iterations is None or iteration < config.max_iterations
|
|
1098
|
+
|
|
1099
|
+
|
|
967
1100
|
def optimize_loaded_text_candidate(
|
|
968
1101
|
*,
|
|
969
1102
|
run_id: str,
|
|
@@ -998,6 +1131,7 @@ def optimize_loaded_text_candidate(
|
|
|
998
1131
|
metric_key = _metric_key(scorer_context)
|
|
999
1132
|
higher_is_better = scorer_context.higher_is_better
|
|
1000
1133
|
budget_exhausted = False
|
|
1134
|
+
failed_reflection_count = 0
|
|
1001
1135
|
|
|
1002
1136
|
try:
|
|
1003
1137
|
event_sink.log_event("run_started", {
|
|
@@ -1095,7 +1229,8 @@ def optimize_loaded_text_candidate(
|
|
|
1095
1229
|
)
|
|
1096
1230
|
budget_exhausted = True
|
|
1097
1231
|
|
|
1098
|
-
|
|
1232
|
+
iteration = 1
|
|
1233
|
+
while config.max_iterations is None or iteration <= config.max_iterations:
|
|
1099
1234
|
if not budget.allows_iteration():
|
|
1100
1235
|
break
|
|
1101
1236
|
|
|
@@ -1215,6 +1350,7 @@ def optimize_loaded_text_candidate(
|
|
|
1215
1350
|
iteration=iteration,
|
|
1216
1351
|
candidate_id=parent_candidate_id,
|
|
1217
1352
|
)
|
|
1353
|
+
budget.used_iterations += 1
|
|
1218
1354
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
1219
1355
|
event_sink.log_event(
|
|
1220
1356
|
"iteration_completed",
|
|
@@ -1233,7 +1369,13 @@ def optimize_loaded_text_candidate(
|
|
|
1233
1369
|
iteration=iteration,
|
|
1234
1370
|
candidate_id=best_candidate_id,
|
|
1235
1371
|
)
|
|
1236
|
-
|
|
1372
|
+
_log_iteration_progress(
|
|
1373
|
+
event_sink,
|
|
1374
|
+
budget=budget,
|
|
1375
|
+
iteration=iteration,
|
|
1376
|
+
candidate_id=best_candidate_id,
|
|
1377
|
+
)
|
|
1378
|
+
if _has_future_iteration(config, iteration) and not budget.allows_iteration():
|
|
1237
1379
|
event_sink.log_event(
|
|
1238
1380
|
"budget_exhausted",
|
|
1239
1381
|
{
|
|
@@ -1248,6 +1390,7 @@ def optimize_loaded_text_candidate(
|
|
|
1248
1390
|
)
|
|
1249
1391
|
budget_exhausted = True
|
|
1250
1392
|
break
|
|
1393
|
+
iteration += 1
|
|
1251
1394
|
continue
|
|
1252
1395
|
|
|
1253
1396
|
event_sink.log_event(
|
|
@@ -1261,7 +1404,68 @@ def optimize_loaded_text_candidate(
|
|
|
1261
1404
|
iteration=iteration,
|
|
1262
1405
|
candidate_id=parent_candidate_id,
|
|
1263
1406
|
)
|
|
1264
|
-
|
|
1407
|
+
try:
|
|
1408
|
+
reflection = reflector(parent_text, parent_results, config)
|
|
1409
|
+
except RetryableReflectionError as error:
|
|
1410
|
+
failed_reflection_count += 1
|
|
1411
|
+
budget.used_candidate_proposals += 1
|
|
1412
|
+
budget.used_iterations += 1
|
|
1413
|
+
budget.charge_retryable_reflection_failure()
|
|
1414
|
+
event_sink.log_event(
|
|
1415
|
+
"reflection_failed",
|
|
1416
|
+
{
|
|
1417
|
+
"parent_candidate_id": parent_candidate_id,
|
|
1418
|
+
"row_ids": [row.id for row in minibatch],
|
|
1419
|
+
"error": str(error),
|
|
1420
|
+
"error_type": type(error).__name__,
|
|
1421
|
+
"budget": budget.to_payload(),
|
|
1422
|
+
},
|
|
1423
|
+
event_layer="extension",
|
|
1424
|
+
iteration=iteration,
|
|
1425
|
+
candidate_id=parent_candidate_id,
|
|
1426
|
+
)
|
|
1427
|
+
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
1428
|
+
event_sink.log_event(
|
|
1429
|
+
"iteration_completed",
|
|
1430
|
+
{
|
|
1431
|
+
"parent_candidate_id": parent_candidate_id,
|
|
1432
|
+
"child_candidate_id": None,
|
|
1433
|
+
"parent_train_score_total": parent_total,
|
|
1434
|
+
"child_train_score_total": None,
|
|
1435
|
+
"child_validation_score": None,
|
|
1436
|
+
"best_candidate_id": best_candidate_id,
|
|
1437
|
+
"best_validation_score": best_score,
|
|
1438
|
+
"reflection_failed": True,
|
|
1439
|
+
"reflection_error": str(error),
|
|
1440
|
+
"reflection_error_type": type(error).__name__,
|
|
1441
|
+
"budget": budget.to_payload(),
|
|
1442
|
+
},
|
|
1443
|
+
iteration=iteration,
|
|
1444
|
+
candidate_id=best_candidate_id,
|
|
1445
|
+
)
|
|
1446
|
+
_log_iteration_progress(
|
|
1447
|
+
event_sink,
|
|
1448
|
+
budget=budget,
|
|
1449
|
+
iteration=iteration,
|
|
1450
|
+
candidate_id=best_candidate_id,
|
|
1451
|
+
)
|
|
1452
|
+
if _has_future_iteration(config, iteration) and not budget.allows_iteration():
|
|
1453
|
+
event_sink.log_event(
|
|
1454
|
+
"budget_exhausted",
|
|
1455
|
+
{
|
|
1456
|
+
"stage": "iteration_completed",
|
|
1457
|
+
"completed_iteration": iteration,
|
|
1458
|
+
"budget": budget.to_payload(),
|
|
1459
|
+
"decision_rule": "budget is checked only between iterations",
|
|
1460
|
+
},
|
|
1461
|
+
event_layer="extension",
|
|
1462
|
+
iteration=iteration,
|
|
1463
|
+
candidate_id=best_candidate_id,
|
|
1464
|
+
)
|
|
1465
|
+
budget_exhausted = True
|
|
1466
|
+
break
|
|
1467
|
+
iteration += 1
|
|
1468
|
+
continue
|
|
1265
1469
|
child_id = _candidate_id(iteration, reflection.candidate_text)
|
|
1266
1470
|
child_text = reflection.candidate_text
|
|
1267
1471
|
candidate_text_by_id[child_id] = child_text
|
|
@@ -1523,6 +1727,7 @@ def optimize_loaded_text_candidate(
|
|
|
1523
1727
|
parent_candidate_id=parent_candidate_id,
|
|
1524
1728
|
)
|
|
1525
1729
|
|
|
1730
|
+
budget.used_iterations += 1
|
|
1526
1731
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
1527
1732
|
event_sink.log_event(
|
|
1528
1733
|
"iteration_completed",
|
|
@@ -1539,7 +1744,13 @@ def optimize_loaded_text_candidate(
|
|
|
1539
1744
|
iteration=iteration,
|
|
1540
1745
|
candidate_id=best_candidate_id,
|
|
1541
1746
|
)
|
|
1542
|
-
|
|
1747
|
+
_log_iteration_progress(
|
|
1748
|
+
event_sink,
|
|
1749
|
+
budget=budget,
|
|
1750
|
+
iteration=iteration,
|
|
1751
|
+
candidate_id=best_candidate_id,
|
|
1752
|
+
)
|
|
1753
|
+
if _has_future_iteration(config, iteration) and not budget.allows_iteration():
|
|
1543
1754
|
event_sink.log_event(
|
|
1544
1755
|
"budget_exhausted",
|
|
1545
1756
|
{
|
|
@@ -1554,6 +1765,7 @@ def optimize_loaded_text_candidate(
|
|
|
1554
1765
|
)
|
|
1555
1766
|
budget_exhausted = True
|
|
1556
1767
|
break
|
|
1768
|
+
iteration += 1
|
|
1557
1769
|
|
|
1558
1770
|
if budget_exhausted:
|
|
1559
1771
|
event_sink.log_event(
|
|
@@ -1565,6 +1777,7 @@ def optimize_loaded_text_candidate(
|
|
|
1565
1777
|
"seed_validation_score": seed_score,
|
|
1566
1778
|
"higher_is_better": higher_is_better,
|
|
1567
1779
|
"budget": budget.to_payload(),
|
|
1780
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1568
1781
|
},
|
|
1569
1782
|
)
|
|
1570
1783
|
event_sink.finish_run(
|
|
@@ -1575,6 +1788,7 @@ def optimize_loaded_text_candidate(
|
|
|
1575
1788
|
"seed_score": seed_score,
|
|
1576
1789
|
"budget": budget.to_payload(),
|
|
1577
1790
|
"pause_reason": "budget_exhausted",
|
|
1791
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1578
1792
|
},
|
|
1579
1793
|
)
|
|
1580
1794
|
return TextGepaResult(
|
|
@@ -1613,6 +1827,7 @@ def optimize_loaded_text_candidate(
|
|
|
1613
1827
|
"metric_key": metric_key,
|
|
1614
1828
|
"higher_is_better": higher_is_better,
|
|
1615
1829
|
"budget": budget.to_payload(),
|
|
1830
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1616
1831
|
},
|
|
1617
1832
|
)
|
|
1618
1833
|
event_sink.finish_run(
|
|
@@ -1625,6 +1840,7 @@ def optimize_loaded_text_candidate(
|
|
|
1625
1840
|
"metric_key": metric_key,
|
|
1626
1841
|
"higher_is_better": higher_is_better,
|
|
1627
1842
|
"budget": budget.to_payload(),
|
|
1843
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1628
1844
|
},
|
|
1629
1845
|
)
|
|
1630
1846
|
return TextGepaResult(
|