orizu 0.3.6 → 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 +6 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +66 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +37 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +645 -27
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +98 -17
- package/vendor/skills/orizu-cli/SKILL.md +8 -2
- package/vendor/skills/orizu-cli/references/cli-reference.md +25 -3
- package/vendor/skills/orizu-cli/references/optimization-reports.md +3 -3
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +19 -5
|
@@ -4,9 +4,13 @@ import dataclasses
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
6
|
import math
|
|
7
|
+
import os
|
|
7
8
|
import random
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
8
11
|
import time
|
|
9
12
|
import uuid
|
|
13
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
10
14
|
from dataclasses import dataclass, field
|
|
11
15
|
from typing import Any, Callable, Protocol
|
|
12
16
|
|
|
@@ -40,6 +44,31 @@ Return only the complete updated parameter text, exactly as it should be used by
|
|
|
40
44
|
Do not include analysis, explanation, labels, XML tags, or a surrounding markdown code fence.
|
|
41
45
|
Your response will be used verbatim as the next candidate."""
|
|
42
46
|
|
|
47
|
+
AUTO_NUM_THREADS_HARD_CAP = 64
|
|
48
|
+
AUTO_NUM_THREADS_WORKER_MEMORY_BYTES = 512 * 1024 * 1024
|
|
49
|
+
AUTO_NUM_THREADS_MEMORY_RESERVE_BYTES = 1024 * 1024 * 1024
|
|
50
|
+
AUTO_NUM_THREADS_FD_HEADROOM = 64
|
|
51
|
+
AUTO_NUM_THREADS_FDS_PER_WORKER = 16
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class NumThreadsPlan:
|
|
56
|
+
requested: int | str
|
|
57
|
+
resolved: int
|
|
58
|
+
row_bound: int
|
|
59
|
+
cpu_bound: int
|
|
60
|
+
memory_bound: int | None
|
|
61
|
+
fd_bound: int | None
|
|
62
|
+
hard_cap: int
|
|
63
|
+
worker_memory_bytes: int
|
|
64
|
+
available_memory_bytes: int | None
|
|
65
|
+
total_memory_bytes: int | None
|
|
66
|
+
memory_reserve_bytes: int | None
|
|
67
|
+
limiting_factor: str
|
|
68
|
+
|
|
69
|
+
def to_payload(self) -> dict[str, Any]:
|
|
70
|
+
return dataclasses.asdict(self)
|
|
71
|
+
|
|
43
72
|
|
|
44
73
|
@dataclass(frozen=True)
|
|
45
74
|
class DatasetRow:
|
|
@@ -113,29 +142,128 @@ class ReflectionResult:
|
|
|
113
142
|
candidate_text: str
|
|
114
143
|
|
|
115
144
|
|
|
145
|
+
class RetryableReflectionError(RuntimeError):
|
|
146
|
+
"""Raised when a transient reflection-provider failure remains after retries."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dspy_auto_metric_budget(
|
|
150
|
+
*,
|
|
151
|
+
num_components: int,
|
|
152
|
+
num_candidates: int,
|
|
153
|
+
valset_size: int,
|
|
154
|
+
minibatch_size: int = 35,
|
|
155
|
+
full_eval_steps: int = 5,
|
|
156
|
+
) -> int:
|
|
157
|
+
if num_candidates <= 0:
|
|
158
|
+
raise ValueError("num_candidates must be > 0.")
|
|
159
|
+
if num_components < 0 or valset_size < 0 or minibatch_size < 0:
|
|
160
|
+
raise ValueError("num_components, valset_size, and minibatch_size must be >= 0.")
|
|
161
|
+
if full_eval_steps < 1:
|
|
162
|
+
raise ValueError("full_eval_steps must be >= 1.")
|
|
163
|
+
|
|
164
|
+
num_trials = int(max(2 * (num_components * 2) * math.log2(num_candidates), 1.5 * num_candidates))
|
|
165
|
+
total = valset_size
|
|
166
|
+
total += num_candidates * 5
|
|
167
|
+
total += num_trials * minibatch_size
|
|
168
|
+
if num_trials == 0:
|
|
169
|
+
return total
|
|
170
|
+
|
|
171
|
+
periodic_fulls = (num_trials + 1) // full_eval_steps + 1
|
|
172
|
+
extra_final = 1 if num_trials < full_eval_steps else 0
|
|
173
|
+
total += (periodic_fulls + extra_final) * valset_size
|
|
174
|
+
return total
|
|
175
|
+
|
|
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
|
+
|
|
116
194
|
@dataclass
|
|
117
195
|
class Budget:
|
|
118
196
|
budget_kind: str
|
|
119
197
|
limit: int
|
|
198
|
+
approx_metric_call_limit: int | None = None
|
|
120
199
|
used_metric_calls: int = 0
|
|
200
|
+
used_reflection_failure_metric_charges: int = 0
|
|
121
201
|
used_full_evals: int = 0
|
|
122
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
|
|
123
208
|
|
|
124
209
|
@classmethod
|
|
125
|
-
def from_config(
|
|
210
|
+
def from_config(
|
|
211
|
+
cls,
|
|
212
|
+
config: "TextGepaConfig",
|
|
213
|
+
*,
|
|
214
|
+
trainset_size: int = 0,
|
|
215
|
+
valset_size: int = 0,
|
|
216
|
+
num_components: int = 1,
|
|
217
|
+
) -> "Budget":
|
|
126
218
|
if config.max_metric_calls is not None:
|
|
127
|
-
return cls(
|
|
219
|
+
return cls(
|
|
220
|
+
"max_metric_calls",
|
|
221
|
+
config.max_metric_calls,
|
|
222
|
+
approx_metric_call_limit=config.max_metric_calls,
|
|
223
|
+
)
|
|
128
224
|
if config.max_full_evals is not None:
|
|
129
|
-
|
|
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
|
+
)
|
|
130
231
|
if config.max_candidate_proposals is not None:
|
|
131
|
-
return cls(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.")
|
|
246
|
+
preset = "medium" if config.budget == "auto" else config.budget
|
|
247
|
+
preset_candidates = {
|
|
248
|
+
"light": 6,
|
|
249
|
+
"medium": 12,
|
|
250
|
+
"heavy": 18,
|
|
251
|
+
"high": 18,
|
|
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
|
+
)
|
|
258
|
+
return cls(
|
|
259
|
+
"max_metric_calls",
|
|
260
|
+
metric_limit,
|
|
261
|
+
approx_metric_call_limit=metric_limit,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def metric_budget_used(self) -> int:
|
|
266
|
+
return self.used_metric_calls + self.used_reflection_failure_metric_charges
|
|
139
267
|
|
|
140
268
|
@property
|
|
141
269
|
def used(self) -> int:
|
|
@@ -143,17 +271,41 @@ class Budget:
|
|
|
143
271
|
return self.used_full_evals
|
|
144
272
|
if self.budget_kind == "max_candidate_proposals":
|
|
145
273
|
return self.used_candidate_proposals
|
|
146
|
-
|
|
274
|
+
if self.budget_kind == "max_iterations":
|
|
275
|
+
return self.used_iterations
|
|
276
|
+
return self.metric_budget_used
|
|
147
277
|
|
|
148
278
|
@property
|
|
149
279
|
def remaining(self) -> int:
|
|
150
280
|
return max(0, self.limit - self.used)
|
|
151
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
|
+
|
|
152
303
|
def allows_iteration(self) -> bool:
|
|
153
304
|
return self.remaining > 0
|
|
154
305
|
|
|
155
|
-
def
|
|
156
|
-
|
|
306
|
+
def charge_retryable_reflection_failure(self) -> None:
|
|
307
|
+
if self.budget_kind == "max_metric_calls":
|
|
308
|
+
self.used_reflection_failure_metric_charges += 1
|
|
157
309
|
|
|
158
310
|
def to_payload(self) -> dict[str, Any]:
|
|
159
311
|
return {
|
|
@@ -161,17 +313,45 @@ class Budget:
|
|
|
161
313
|
"limit": self.limit,
|
|
162
314
|
"used": self.used,
|
|
163
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,
|
|
164
320
|
"used_metric_calls": self.used_metric_calls,
|
|
321
|
+
"used_reflection_failure_metric_charges": self.used_reflection_failure_metric_charges,
|
|
165
322
|
"used_full_evals": self.used_full_evals,
|
|
166
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(),
|
|
167
346
|
}
|
|
168
347
|
|
|
169
348
|
|
|
170
349
|
@dataclass(frozen=True)
|
|
171
350
|
class TextGepaConfig:
|
|
172
|
-
budget: str = "
|
|
173
|
-
max_iterations: int =
|
|
351
|
+
budget: str | None = "auto"
|
|
352
|
+
max_iterations: int | None = None
|
|
174
353
|
minibatch_size: int = 3
|
|
354
|
+
num_threads: int | str = "auto"
|
|
175
355
|
candidate_selection_strategy: str = "pareto"
|
|
176
356
|
epsilon: float = 0.1
|
|
177
357
|
max_metric_calls: int | None = None
|
|
@@ -180,6 +360,8 @@ class TextGepaConfig:
|
|
|
180
360
|
reflection_model: str = "anthropic/claude-opus-4-7"
|
|
181
361
|
reflection_temperature: float | None = None
|
|
182
362
|
reflection_max_tokens: int | None = None
|
|
363
|
+
reflection_retry_attempts: int = 3
|
|
364
|
+
reflection_http_timeout_seconds: int = 180
|
|
183
365
|
reflection_prompt_template: str | None = None
|
|
184
366
|
reflection_provider_settings: dict[str, Any] = field(default_factory=dict)
|
|
185
367
|
objective: str = "Improve this text candidate to maximize evaluator score while preserving intended behavior."
|
|
@@ -316,6 +498,230 @@ def _metric_key(scorer_context: PromptContext) -> str:
|
|
|
316
498
|
return scorer_context.metric_key or "score"
|
|
317
499
|
|
|
318
500
|
|
|
501
|
+
def _positive_int_env(name: str, default: int) -> int:
|
|
502
|
+
raw = os.environ.get(name)
|
|
503
|
+
if raw is None:
|
|
504
|
+
return default
|
|
505
|
+
try:
|
|
506
|
+
value = int(raw)
|
|
507
|
+
except ValueError:
|
|
508
|
+
return default
|
|
509
|
+
return value if value > 0 else default
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _proc_meminfo_bytes(key: str) -> int | None:
|
|
513
|
+
try:
|
|
514
|
+
with open("/proc/meminfo", encoding="utf-8") as handle:
|
|
515
|
+
for line in handle:
|
|
516
|
+
if not line.startswith(f"{key}:"):
|
|
517
|
+
continue
|
|
518
|
+
parts = line.split()
|
|
519
|
+
if len(parts) >= 2:
|
|
520
|
+
return int(parts[1]) * 1024
|
|
521
|
+
except (OSError, ValueError):
|
|
522
|
+
return None
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _sysconf_memory_bytes(pages_name: str) -> int | None:
|
|
527
|
+
try:
|
|
528
|
+
pages = os.sysconf(pages_name)
|
|
529
|
+
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
530
|
+
except (AttributeError, OSError, ValueError):
|
|
531
|
+
return None
|
|
532
|
+
if not isinstance(pages, int) or not isinstance(page_size, int):
|
|
533
|
+
return None
|
|
534
|
+
if pages <= 0 or page_size <= 0:
|
|
535
|
+
return None
|
|
536
|
+
return pages * page_size
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _darwin_available_memory_bytes() -> int | None:
|
|
540
|
+
if sys.platform != "darwin":
|
|
541
|
+
return None
|
|
542
|
+
try:
|
|
543
|
+
output = subprocess.check_output(["vm_stat"], text=True)
|
|
544
|
+
except (OSError, subprocess.SubprocessError):
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
lines = output.splitlines()
|
|
548
|
+
if not lines:
|
|
549
|
+
return None
|
|
550
|
+
page_size_marker = "page size of "
|
|
551
|
+
if page_size_marker not in lines[0]:
|
|
552
|
+
return None
|
|
553
|
+
try:
|
|
554
|
+
page_size = int(lines[0].split(page_size_marker, 1)[1].split()[0])
|
|
555
|
+
except (IndexError, ValueError):
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
available_pages = 0
|
|
559
|
+
available_keys = {
|
|
560
|
+
"Pages free",
|
|
561
|
+
"Pages inactive",
|
|
562
|
+
"Pages speculative",
|
|
563
|
+
"Pages purgeable",
|
|
564
|
+
}
|
|
565
|
+
for line in lines[1:]:
|
|
566
|
+
key, separator, value = line.partition(":")
|
|
567
|
+
if separator != ":" or key not in available_keys:
|
|
568
|
+
continue
|
|
569
|
+
normalized_value = value.strip().rstrip(".").replace(",", "")
|
|
570
|
+
try:
|
|
571
|
+
available_pages += int(normalized_value)
|
|
572
|
+
except ValueError:
|
|
573
|
+
continue
|
|
574
|
+
return available_pages * page_size if available_pages > 0 else None
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _available_memory_bytes() -> int | None:
|
|
578
|
+
return (
|
|
579
|
+
_proc_meminfo_bytes("MemAvailable")
|
|
580
|
+
or _darwin_available_memory_bytes()
|
|
581
|
+
or _sysconf_memory_bytes("SC_AVPHYS_PAGES")
|
|
582
|
+
or _total_memory_bytes()
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _total_memory_bytes() -> int | None:
|
|
587
|
+
return _proc_meminfo_bytes("MemTotal") or _sysconf_memory_bytes("SC_PHYS_PAGES")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _open_file_soft_limit() -> int | None:
|
|
591
|
+
try:
|
|
592
|
+
import resource
|
|
593
|
+
except ImportError:
|
|
594
|
+
return None
|
|
595
|
+
try:
|
|
596
|
+
soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
597
|
+
except (OSError, ValueError):
|
|
598
|
+
return None
|
|
599
|
+
if soft_limit <= 0 or soft_limit == resource.RLIM_INFINITY:
|
|
600
|
+
return None
|
|
601
|
+
return int(soft_limit)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _parse_requested_num_threads(requested: int | str) -> int | str:
|
|
605
|
+
if isinstance(requested, int):
|
|
606
|
+
if requested <= 0:
|
|
607
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
608
|
+
return requested
|
|
609
|
+
if isinstance(requested, str):
|
|
610
|
+
normalized = requested.strip().lower()
|
|
611
|
+
if normalized == "auto":
|
|
612
|
+
return "auto"
|
|
613
|
+
try:
|
|
614
|
+
parsed = int(normalized)
|
|
615
|
+
except ValueError as exc:
|
|
616
|
+
raise ValueError("num_threads must be positive or 'auto'") from exc
|
|
617
|
+
if parsed <= 0:
|
|
618
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
619
|
+
return parsed
|
|
620
|
+
raise ValueError("num_threads must be positive or 'auto'")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def resolve_num_threads(
|
|
624
|
+
requested: int | str,
|
|
625
|
+
*,
|
|
626
|
+
minibatch_size: int,
|
|
627
|
+
validation_count: int,
|
|
628
|
+
cpu_count: int | None = None,
|
|
629
|
+
available_memory_bytes: int | None = None,
|
|
630
|
+
total_memory_bytes: int | None = None,
|
|
631
|
+
fd_limit: int | None = None,
|
|
632
|
+
hard_cap: int | None = None,
|
|
633
|
+
worker_memory_bytes: int | None = None,
|
|
634
|
+
) -> NumThreadsPlan:
|
|
635
|
+
"""Resolve an explicit or automatic row-evaluation thread count for one GEPA run."""
|
|
636
|
+
parsed = _parse_requested_num_threads(requested)
|
|
637
|
+
row_bound = max(1, max(minibatch_size, validation_count))
|
|
638
|
+
detected_cpu_count = cpu_count if cpu_count is not None else os.cpu_count()
|
|
639
|
+
normalized_cpu_count = max(1, detected_cpu_count or 1)
|
|
640
|
+
cpu_bound = normalized_cpu_count * 2
|
|
641
|
+
resolved_hard_cap = hard_cap or _positive_int_env(
|
|
642
|
+
"ORIZU_GEPA_AUTO_THREADS_MAX",
|
|
643
|
+
AUTO_NUM_THREADS_HARD_CAP,
|
|
644
|
+
)
|
|
645
|
+
resolved_worker_memory_bytes = worker_memory_bytes or (
|
|
646
|
+
_positive_int_env(
|
|
647
|
+
"ORIZU_GEPA_WORKER_MEMORY_MB",
|
|
648
|
+
AUTO_NUM_THREADS_WORKER_MEMORY_BYTES // (1024 * 1024),
|
|
649
|
+
) * 1024 * 1024
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
detected_total_memory_bytes = (
|
|
653
|
+
total_memory_bytes if total_memory_bytes is not None else _total_memory_bytes()
|
|
654
|
+
)
|
|
655
|
+
detected_available_memory_bytes = (
|
|
656
|
+
available_memory_bytes
|
|
657
|
+
if available_memory_bytes is not None
|
|
658
|
+
else _available_memory_bytes()
|
|
659
|
+
)
|
|
660
|
+
memory_reserve_bytes = None
|
|
661
|
+
memory_bound = None
|
|
662
|
+
if detected_available_memory_bytes is not None:
|
|
663
|
+
total_reserve = (
|
|
664
|
+
detected_total_memory_bytes // 4
|
|
665
|
+
if detected_total_memory_bytes is not None
|
|
666
|
+
else 0
|
|
667
|
+
)
|
|
668
|
+
memory_reserve_bytes = max(AUTO_NUM_THREADS_MEMORY_RESERVE_BYTES, total_reserve)
|
|
669
|
+
usable_memory_bytes = max(0, detected_available_memory_bytes - memory_reserve_bytes)
|
|
670
|
+
memory_bound = max(1, usable_memory_bytes // max(1, resolved_worker_memory_bytes))
|
|
671
|
+
|
|
672
|
+
detected_fd_limit = fd_limit if fd_limit is not None else _open_file_soft_limit()
|
|
673
|
+
fd_bound = None
|
|
674
|
+
if detected_fd_limit is not None:
|
|
675
|
+
usable_fds = max(0, detected_fd_limit - AUTO_NUM_THREADS_FD_HEADROOM)
|
|
676
|
+
fd_bound = max(1, usable_fds // AUTO_NUM_THREADS_FDS_PER_WORKER)
|
|
677
|
+
|
|
678
|
+
if parsed != "auto":
|
|
679
|
+
return NumThreadsPlan(
|
|
680
|
+
requested=requested,
|
|
681
|
+
resolved=parsed,
|
|
682
|
+
row_bound=row_bound,
|
|
683
|
+
cpu_bound=cpu_bound,
|
|
684
|
+
memory_bound=memory_bound,
|
|
685
|
+
fd_bound=fd_bound,
|
|
686
|
+
hard_cap=resolved_hard_cap,
|
|
687
|
+
worker_memory_bytes=resolved_worker_memory_bytes,
|
|
688
|
+
available_memory_bytes=detected_available_memory_bytes,
|
|
689
|
+
total_memory_bytes=detected_total_memory_bytes,
|
|
690
|
+
memory_reserve_bytes=memory_reserve_bytes,
|
|
691
|
+
limiting_factor="explicit",
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
limits = {
|
|
695
|
+
"rows": row_bound,
|
|
696
|
+
"cpu": cpu_bound,
|
|
697
|
+
"hard_cap": resolved_hard_cap,
|
|
698
|
+
}
|
|
699
|
+
if memory_bound is not None:
|
|
700
|
+
limits["memory"] = memory_bound
|
|
701
|
+
if fd_bound is not None:
|
|
702
|
+
limits["file_descriptors"] = fd_bound
|
|
703
|
+
resolved = max(1, min(limits.values()))
|
|
704
|
+
limiting_factor = next(
|
|
705
|
+
name
|
|
706
|
+
for name in ("rows", "cpu", "memory", "file_descriptors", "hard_cap")
|
|
707
|
+
if limits.get(name) == resolved
|
|
708
|
+
)
|
|
709
|
+
return NumThreadsPlan(
|
|
710
|
+
requested=requested,
|
|
711
|
+
resolved=resolved,
|
|
712
|
+
row_bound=row_bound,
|
|
713
|
+
cpu_bound=cpu_bound,
|
|
714
|
+
memory_bound=memory_bound,
|
|
715
|
+
fd_bound=fd_bound,
|
|
716
|
+
hard_cap=resolved_hard_cap,
|
|
717
|
+
worker_memory_bytes=resolved_worker_memory_bytes,
|
|
718
|
+
available_memory_bytes=detected_available_memory_bytes,
|
|
719
|
+
total_memory_bytes=detected_total_memory_bytes,
|
|
720
|
+
memory_reserve_bytes=memory_reserve_bytes,
|
|
721
|
+
limiting_factor=limiting_factor,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
319
725
|
def _is_better(challenger: float, incumbent: float, higher_is_better: bool) -> bool:
|
|
320
726
|
return challenger > incumbent if higher_is_better else challenger < incumbent
|
|
321
727
|
|
|
@@ -564,12 +970,13 @@ def evaluate_candidate(
|
|
|
564
970
|
scorer_runner: ScorerRunner,
|
|
565
971
|
budget: Budget,
|
|
566
972
|
evaluation_cache: EvaluationCache | None = None,
|
|
973
|
+
num_threads: int = 1,
|
|
567
974
|
) -> list[RowEvaluation]:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
break
|
|
975
|
+
"""Evaluate one candidate over rows, preserving row order while parallelizing uncached calls."""
|
|
976
|
+
evaluations_by_index: dict[int, RowEvaluation] = {}
|
|
977
|
+
pending: list[tuple[int, DatasetRow, str | None]] = []
|
|
572
978
|
|
|
979
|
+
for index, row in enumerate(rows):
|
|
573
980
|
cache_key = None
|
|
574
981
|
if evaluation_cache is not None:
|
|
575
982
|
cache_key = evaluation_cache.key(
|
|
@@ -581,9 +988,12 @@ def evaluate_candidate(
|
|
|
581
988
|
)
|
|
582
989
|
cached = evaluation_cache.get(cache_key)
|
|
583
990
|
if cached is not None:
|
|
584
|
-
|
|
991
|
+
evaluations_by_index[index] = cached
|
|
585
992
|
continue
|
|
586
993
|
|
|
994
|
+
pending.append((index, row, cache_key))
|
|
995
|
+
|
|
996
|
+
def run_uncached_row(index: int, row: DatasetRow, cache_key: str | None) -> tuple[int, str | None, RowEvaluation]:
|
|
587
997
|
started = time.time()
|
|
588
998
|
candidate_result = candidate_runner(candidate_text, row, prompt_context, candidate_id)
|
|
589
999
|
scorer_result = scorer_runner(row, candidate_result, scorer_context, candidate_id)
|
|
@@ -610,17 +1020,83 @@ def evaluate_candidate(
|
|
|
610
1020
|
if candidate_result.cost_usd is not None or scorer_result.cost_usd is not None else None,
|
|
611
1021
|
error=candidate_result.error or scorer_result.error,
|
|
612
1022
|
)
|
|
613
|
-
|
|
1023
|
+
return index, cache_key, evaluation
|
|
1024
|
+
|
|
1025
|
+
worker_count = max(1, min(max(1, num_threads), len(pending) or 1))
|
|
1026
|
+
if worker_count == 1:
|
|
1027
|
+
completed = [run_uncached_row(index, row, cache_key) for index, row, cache_key in pending]
|
|
1028
|
+
else:
|
|
1029
|
+
completed = []
|
|
1030
|
+
next_pending_index = 0
|
|
1031
|
+
executor = ThreadPoolExecutor(max_workers=worker_count)
|
|
1032
|
+
futures = set()
|
|
1033
|
+
|
|
1034
|
+
def submit_next_pending() -> bool:
|
|
1035
|
+
nonlocal next_pending_index
|
|
1036
|
+
if next_pending_index >= len(pending):
|
|
1037
|
+
return False
|
|
1038
|
+
index, row, cache_key = pending[next_pending_index]
|
|
1039
|
+
next_pending_index += 1
|
|
1040
|
+
futures.add(executor.submit(run_uncached_row, index, row, cache_key))
|
|
1041
|
+
return True
|
|
1042
|
+
|
|
1043
|
+
try:
|
|
1044
|
+
for _ in range(worker_count):
|
|
1045
|
+
if not submit_next_pending():
|
|
1046
|
+
break
|
|
1047
|
+
while futures:
|
|
1048
|
+
done, _ = wait(futures, return_when=FIRST_COMPLETED)
|
|
1049
|
+
batch = []
|
|
1050
|
+
for future in done:
|
|
1051
|
+
futures.remove(future)
|
|
1052
|
+
batch.append(future.result())
|
|
1053
|
+
completed.extend(batch)
|
|
1054
|
+
for _ in batch:
|
|
1055
|
+
submit_next_pending()
|
|
1056
|
+
except BaseException:
|
|
1057
|
+
for future in futures:
|
|
1058
|
+
future.cancel()
|
|
1059
|
+
executor.shutdown(wait=True)
|
|
1060
|
+
raise
|
|
1061
|
+
else:
|
|
1062
|
+
executor.shutdown(wait=True)
|
|
1063
|
+
|
|
1064
|
+
for index, cache_key, evaluation in completed:
|
|
1065
|
+
evaluations_by_index[index] = evaluation
|
|
614
1066
|
if evaluation_cache is not None and cache_key is not None:
|
|
615
1067
|
evaluation_cache.set(cache_key, evaluation)
|
|
616
1068
|
budget.used_metric_calls += 1
|
|
617
|
-
|
|
1069
|
+
|
|
1070
|
+
return [
|
|
1071
|
+
evaluations_by_index[index]
|
|
1072
|
+
for index in sorted(evaluations_by_index)
|
|
1073
|
+
]
|
|
1074
|
+
|
|
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
|
+
)
|
|
618
1090
|
|
|
619
1091
|
|
|
620
1092
|
def _completed_all_rows(results: list[RowEvaluation], rows: list[DatasetRow]) -> bool:
|
|
621
1093
|
return len(results) == len(rows)
|
|
622
1094
|
|
|
623
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
|
+
|
|
624
1100
|
def optimize_loaded_text_candidate(
|
|
625
1101
|
*,
|
|
626
1102
|
run_id: str,
|
|
@@ -636,7 +1112,17 @@ def optimize_loaded_text_candidate(
|
|
|
636
1112
|
local_logger: Any | None = None,
|
|
637
1113
|
) -> TextGepaResult:
|
|
638
1114
|
seed_text = prompt_context.body or ""
|
|
639
|
-
budget = Budget.from_config(
|
|
1115
|
+
budget = Budget.from_config(
|
|
1116
|
+
config,
|
|
1117
|
+
trainset_size=len(trainset),
|
|
1118
|
+
valset_size=len(valset),
|
|
1119
|
+
num_components=1,
|
|
1120
|
+
)
|
|
1121
|
+
num_threads_plan = resolve_num_threads(
|
|
1122
|
+
config.num_threads,
|
|
1123
|
+
minibatch_size=config.minibatch_size,
|
|
1124
|
+
validation_count=len(valset),
|
|
1125
|
+
)
|
|
640
1126
|
evaluation_cache = EvaluationCache() if config.cache_evaluations else None
|
|
641
1127
|
candidate_text_by_id = {"seed": seed_text}
|
|
642
1128
|
val_scores_by_candidate: dict[str, dict[str, float]] = {}
|
|
@@ -645,6 +1131,7 @@ def optimize_loaded_text_candidate(
|
|
|
645
1131
|
metric_key = _metric_key(scorer_context)
|
|
646
1132
|
higher_is_better = scorer_context.higher_is_better
|
|
647
1133
|
budget_exhausted = False
|
|
1134
|
+
failed_reflection_count = 0
|
|
648
1135
|
|
|
649
1136
|
try:
|
|
650
1137
|
event_sink.log_event("run_started", {
|
|
@@ -664,6 +1151,7 @@ def optimize_loaded_text_candidate(
|
|
|
664
1151
|
"train_count": len(trainset),
|
|
665
1152
|
"validation_count": len(valset),
|
|
666
1153
|
"config": dataclasses.asdict(config),
|
|
1154
|
+
"num_threads": num_threads_plan.to_payload(),
|
|
667
1155
|
"budget": budget.to_payload(),
|
|
668
1156
|
})
|
|
669
1157
|
|
|
@@ -684,6 +1172,7 @@ def optimize_loaded_text_candidate(
|
|
|
684
1172
|
scorer_runner=scorer_runner,
|
|
685
1173
|
budget=budget,
|
|
686
1174
|
evaluation_cache=evaluation_cache,
|
|
1175
|
+
num_threads=num_threads_plan.resolved,
|
|
687
1176
|
)
|
|
688
1177
|
if local_logger is not None:
|
|
689
1178
|
local_logger.append_evaluations(
|
|
@@ -694,7 +1183,7 @@ def optimize_loaded_text_candidate(
|
|
|
694
1183
|
results=seed_results,
|
|
695
1184
|
)
|
|
696
1185
|
if not _completed_all_rows(seed_results, valset):
|
|
697
|
-
raise RuntimeError("
|
|
1186
|
+
raise RuntimeError("Seed validation did not complete all requested rows")
|
|
698
1187
|
budget.used_full_evals += 1
|
|
699
1188
|
seed_score = _mean(seed_results)
|
|
700
1189
|
val_scores_by_candidate["seed"] = {result.row_id: result.score for result in seed_results}
|
|
@@ -725,8 +1214,23 @@ def optimize_loaded_text_candidate(
|
|
|
725
1214
|
candidate_id=best_candidate_id,
|
|
726
1215
|
)
|
|
727
1216
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension")
|
|
1217
|
+
if not budget.allows_iteration():
|
|
1218
|
+
event_sink.log_event(
|
|
1219
|
+
"budget_exhausted",
|
|
1220
|
+
{
|
|
1221
|
+
"stage": "seed_val_set_completed",
|
|
1222
|
+
"requested_rows": len(valset),
|
|
1223
|
+
"completed_rows": len(seed_results),
|
|
1224
|
+
"budget": budget.to_payload(),
|
|
1225
|
+
"decision_rule": "budget is checked only after completing seed validation or a full iteration",
|
|
1226
|
+
},
|
|
1227
|
+
event_layer="extension",
|
|
1228
|
+
candidate_id="seed",
|
|
1229
|
+
)
|
|
1230
|
+
budget_exhausted = True
|
|
728
1231
|
|
|
729
|
-
|
|
1232
|
+
iteration = 1
|
|
1233
|
+
while config.max_iterations is None or iteration <= config.max_iterations:
|
|
730
1234
|
if not budget.allows_iteration():
|
|
731
1235
|
break
|
|
732
1236
|
|
|
@@ -787,6 +1291,7 @@ def optimize_loaded_text_candidate(
|
|
|
787
1291
|
scorer_runner=scorer_runner,
|
|
788
1292
|
budget=budget,
|
|
789
1293
|
evaluation_cache=evaluation_cache,
|
|
1294
|
+
num_threads=num_threads_plan.resolved,
|
|
790
1295
|
)
|
|
791
1296
|
if local_logger is not None:
|
|
792
1297
|
local_logger.append_evaluations(
|
|
@@ -845,6 +1350,7 @@ def optimize_loaded_text_candidate(
|
|
|
845
1350
|
iteration=iteration,
|
|
846
1351
|
candidate_id=parent_candidate_id,
|
|
847
1352
|
)
|
|
1353
|
+
budget.used_iterations += 1
|
|
848
1354
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
849
1355
|
event_sink.log_event(
|
|
850
1356
|
"iteration_completed",
|
|
@@ -863,6 +1369,28 @@ def optimize_loaded_text_candidate(
|
|
|
863
1369
|
iteration=iteration,
|
|
864
1370
|
candidate_id=best_candidate_id,
|
|
865
1371
|
)
|
|
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():
|
|
1379
|
+
event_sink.log_event(
|
|
1380
|
+
"budget_exhausted",
|
|
1381
|
+
{
|
|
1382
|
+
"stage": "iteration_completed",
|
|
1383
|
+
"completed_iteration": iteration,
|
|
1384
|
+
"budget": budget.to_payload(),
|
|
1385
|
+
"decision_rule": "budget is checked only between iterations",
|
|
1386
|
+
},
|
|
1387
|
+
event_layer="extension",
|
|
1388
|
+
iteration=iteration,
|
|
1389
|
+
candidate_id=best_candidate_id,
|
|
1390
|
+
)
|
|
1391
|
+
budget_exhausted = True
|
|
1392
|
+
break
|
|
1393
|
+
iteration += 1
|
|
866
1394
|
continue
|
|
867
1395
|
|
|
868
1396
|
event_sink.log_event(
|
|
@@ -876,7 +1404,68 @@ def optimize_loaded_text_candidate(
|
|
|
876
1404
|
iteration=iteration,
|
|
877
1405
|
candidate_id=parent_candidate_id,
|
|
878
1406
|
)
|
|
879
|
-
|
|
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
|
|
880
1469
|
child_id = _candidate_id(iteration, reflection.candidate_text)
|
|
881
1470
|
child_text = reflection.candidate_text
|
|
882
1471
|
candidate_text_by_id[child_id] = child_text
|
|
@@ -946,6 +1535,7 @@ def optimize_loaded_text_candidate(
|
|
|
946
1535
|
scorer_runner=scorer_runner,
|
|
947
1536
|
budget=budget,
|
|
948
1537
|
evaluation_cache=evaluation_cache,
|
|
1538
|
+
num_threads=num_threads_plan.resolved,
|
|
949
1539
|
)
|
|
950
1540
|
if local_logger is not None:
|
|
951
1541
|
local_logger.append_evaluations(
|
|
@@ -1028,6 +1618,7 @@ def optimize_loaded_text_candidate(
|
|
|
1028
1618
|
scorer_runner=scorer_runner,
|
|
1029
1619
|
budget=budget,
|
|
1030
1620
|
evaluation_cache=evaluation_cache,
|
|
1621
|
+
num_threads=num_threads_plan.resolved,
|
|
1031
1622
|
)
|
|
1032
1623
|
if local_logger is not None:
|
|
1033
1624
|
local_logger.append_evaluations(
|
|
@@ -1136,6 +1727,7 @@ def optimize_loaded_text_candidate(
|
|
|
1136
1727
|
parent_candidate_id=parent_candidate_id,
|
|
1137
1728
|
)
|
|
1138
1729
|
|
|
1730
|
+
budget.used_iterations += 1
|
|
1139
1731
|
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
1140
1732
|
event_sink.log_event(
|
|
1141
1733
|
"iteration_completed",
|
|
@@ -1152,6 +1744,28 @@ def optimize_loaded_text_candidate(
|
|
|
1152
1744
|
iteration=iteration,
|
|
1153
1745
|
candidate_id=best_candidate_id,
|
|
1154
1746
|
)
|
|
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():
|
|
1754
|
+
event_sink.log_event(
|
|
1755
|
+
"budget_exhausted",
|
|
1756
|
+
{
|
|
1757
|
+
"stage": "iteration_completed",
|
|
1758
|
+
"completed_iteration": iteration,
|
|
1759
|
+
"budget": budget.to_payload(),
|
|
1760
|
+
"decision_rule": "budget is checked only between iterations",
|
|
1761
|
+
},
|
|
1762
|
+
event_layer="extension",
|
|
1763
|
+
iteration=iteration,
|
|
1764
|
+
candidate_id=best_candidate_id,
|
|
1765
|
+
)
|
|
1766
|
+
budget_exhausted = True
|
|
1767
|
+
break
|
|
1768
|
+
iteration += 1
|
|
1155
1769
|
|
|
1156
1770
|
if budget_exhausted:
|
|
1157
1771
|
event_sink.log_event(
|
|
@@ -1163,6 +1777,7 @@ def optimize_loaded_text_candidate(
|
|
|
1163
1777
|
"seed_validation_score": seed_score,
|
|
1164
1778
|
"higher_is_better": higher_is_better,
|
|
1165
1779
|
"budget": budget.to_payload(),
|
|
1780
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1166
1781
|
},
|
|
1167
1782
|
)
|
|
1168
1783
|
event_sink.finish_run(
|
|
@@ -1173,6 +1788,7 @@ def optimize_loaded_text_candidate(
|
|
|
1173
1788
|
"seed_score": seed_score,
|
|
1174
1789
|
"budget": budget.to_payload(),
|
|
1175
1790
|
"pause_reason": "budget_exhausted",
|
|
1791
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1176
1792
|
},
|
|
1177
1793
|
)
|
|
1178
1794
|
return TextGepaResult(
|
|
@@ -1211,6 +1827,7 @@ def optimize_loaded_text_candidate(
|
|
|
1211
1827
|
"metric_key": metric_key,
|
|
1212
1828
|
"higher_is_better": higher_is_better,
|
|
1213
1829
|
"budget": budget.to_payload(),
|
|
1830
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1214
1831
|
},
|
|
1215
1832
|
)
|
|
1216
1833
|
event_sink.finish_run(
|
|
@@ -1223,6 +1840,7 @@ def optimize_loaded_text_candidate(
|
|
|
1223
1840
|
"metric_key": metric_key,
|
|
1224
1841
|
"higher_is_better": higher_is_better,
|
|
1225
1842
|
"budget": budget.to_payload(),
|
|
1843
|
+
"failed_reflection_count": failed_reflection_count,
|
|
1226
1844
|
},
|
|
1227
1845
|
)
|
|
1228
1846
|
return TextGepaResult(
|