orizu 0.3.7 → 0.4.1
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 +114 -15
- package/dist/index.js +828 -67
- package/dist/setup-prompt.js +22 -0
- package/dist/skill-installer.js +243 -17
- package/dist/task-assignment-manifest.js +59 -0
- 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 +114 -15
- package/src/index.ts +933 -70
- package/src/setup-prompt.ts +27 -0
- package/src/skill-installer.ts +343 -17
- package/src/task-assignment-manifest.ts +96 -0
- 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 +13 -6
- package/vendor/skills/orizu-cli/references/cli-reference.md +34 -5
- 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
|
@@ -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(
|
|
@@ -2,14 +2,31 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import random
|
|
6
|
+
import socket
|
|
5
7
|
import ssl
|
|
6
8
|
import subprocess
|
|
7
9
|
import tempfile
|
|
10
|
+
import time
|
|
8
11
|
import urllib.error
|
|
9
12
|
import urllib.request
|
|
13
|
+
from collections.abc import Callable
|
|
10
14
|
from typing import Any
|
|
11
15
|
|
|
12
|
-
from .optimizer import
|
|
16
|
+
from .optimizer import (
|
|
17
|
+
ReflectionResult,
|
|
18
|
+
RetryableReflectionError,
|
|
19
|
+
RowEvaluation,
|
|
20
|
+
TextGepaConfig,
|
|
21
|
+
build_reflection_prompt,
|
|
22
|
+
extract_candidate_text,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RETRYABLE_HTTP_STATUS_CODES = {429, 500, 502, 503, 504, 529}
|
|
27
|
+
RETRY_BACKOFF_BASE_SECONDS = 5.0
|
|
28
|
+
RETRY_BACKOFF_JITTER_SECONDS = 5.0
|
|
29
|
+
RETRYABLE_DIRECT_ERRORS = (TimeoutError, socket.timeout, ConnectionResetError, ConnectionAbortedError)
|
|
13
30
|
|
|
14
31
|
|
|
15
32
|
def _read_anthropic_response_with_curl(api_key: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -40,6 +57,73 @@ def _read_anthropic_response_with_curl(api_key: str, payload: dict[str, Any]) ->
|
|
|
40
57
|
return json.loads(result.stdout)
|
|
41
58
|
|
|
42
59
|
|
|
60
|
+
def _validate_positive_http_config(value: int, name: str) -> int:
|
|
61
|
+
if value <= 0:
|
|
62
|
+
raise RuntimeError(f"{name} must be positive")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _retry_backoff_seconds(attempt: int) -> float:
|
|
67
|
+
return (RETRY_BACKOFF_BASE_SECONDS * (2 ** attempt)) + random.uniform(0, RETRY_BACKOFF_JITTER_SECONDS)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_retryable_http_status(status_code: int) -> bool:
|
|
71
|
+
return status_code in RETRYABLE_HTTP_STATUS_CODES
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_retryable_url_error(error: urllib.error.URLError) -> bool:
|
|
75
|
+
return isinstance(error.reason, RETRYABLE_DIRECT_ERRORS)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_json_response_with_retries(
|
|
79
|
+
request: urllib.request.Request,
|
|
80
|
+
*,
|
|
81
|
+
timeout_seconds: int,
|
|
82
|
+
retry_attempts: int,
|
|
83
|
+
failure_prefix: str,
|
|
84
|
+
ssl_fallback: Callable[[], dict[str, Any]] | None = None,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
timeout_seconds = _validate_positive_http_config(timeout_seconds, "reflection_http_timeout_seconds")
|
|
87
|
+
retry_attempts = _validate_positive_http_config(retry_attempts, "reflection_retry_attempts")
|
|
88
|
+
|
|
89
|
+
for attempt in range(retry_attempts):
|
|
90
|
+
try:
|
|
91
|
+
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
|
92
|
+
return json.loads(response.read().decode("utf-8"))
|
|
93
|
+
except urllib.error.HTTPError as error:
|
|
94
|
+
if _is_retryable_http_status(error.code) and attempt < retry_attempts - 1:
|
|
95
|
+
time.sleep(_retry_backoff_seconds(attempt))
|
|
96
|
+
continue
|
|
97
|
+
detail = error.read().decode("utf-8", errors="replace")
|
|
98
|
+
if _is_retryable_http_status(error.code):
|
|
99
|
+
raise RetryableReflectionError(
|
|
100
|
+
f"{failure_prefix} retryable HTTP failure after {retry_attempts} attempts: "
|
|
101
|
+
f"{error.code} {detail}"
|
|
102
|
+
) from error
|
|
103
|
+
raise RuntimeError(f"{failure_prefix}: {error.code} {detail}") from error
|
|
104
|
+
except urllib.error.URLError as error:
|
|
105
|
+
if isinstance(error.reason, ssl.SSLCertVerificationError) and ssl_fallback is not None:
|
|
106
|
+
return ssl_fallback()
|
|
107
|
+
if _is_retryable_url_error(error):
|
|
108
|
+
if attempt < retry_attempts - 1:
|
|
109
|
+
time.sleep(_retry_backoff_seconds(attempt))
|
|
110
|
+
continue
|
|
111
|
+
raise RetryableReflectionError(
|
|
112
|
+
f"{failure_prefix} retryable connection failure after {retry_attempts} attempts: "
|
|
113
|
+
f"{error.reason}"
|
|
114
|
+
) from error
|
|
115
|
+
raise
|
|
116
|
+
except RETRYABLE_DIRECT_ERRORS as error:
|
|
117
|
+
if attempt < retry_attempts - 1:
|
|
118
|
+
time.sleep(_retry_backoff_seconds(attempt))
|
|
119
|
+
continue
|
|
120
|
+
raise RetryableReflectionError(
|
|
121
|
+
f"{failure_prefix} timed out after {retry_attempts} attempts: {error}"
|
|
122
|
+
) from error
|
|
123
|
+
|
|
124
|
+
raise RetryableReflectionError(f"{failure_prefix} failed after {retry_attempts} attempts")
|
|
125
|
+
|
|
126
|
+
|
|
43
127
|
def _merge_provider_settings(
|
|
44
128
|
payload: dict[str, Any],
|
|
45
129
|
settings: dict[str, Any],
|
|
@@ -143,16 +227,13 @@ def reflect_with_anthropic(parent_text: str, parent_results: list[RowEvaluation]
|
|
|
143
227
|
"content-type": "application/json",
|
|
144
228
|
},
|
|
145
229
|
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if not isinstance(error.reason, ssl.SSLCertVerificationError):
|
|
154
|
-
raise
|
|
155
|
-
data = _read_anthropic_response_with_curl(api_key, payload)
|
|
230
|
+
data = _read_json_response_with_retries(
|
|
231
|
+
request,
|
|
232
|
+
timeout_seconds=config.reflection_http_timeout_seconds,
|
|
233
|
+
retry_attempts=config.reflection_retry_attempts,
|
|
234
|
+
failure_prefix="Reflection LM failed",
|
|
235
|
+
ssl_fallback=lambda: _read_anthropic_response_with_curl(api_key, payload),
|
|
236
|
+
)
|
|
156
237
|
parts = data.get("content") or []
|
|
157
238
|
text = "".join(part.get("text", "") for part in parts if isinstance(part, dict))
|
|
158
239
|
return ReflectionResult(
|
|
@@ -180,12 +261,12 @@ def reflect_with_openai(parent_text: str, parent_results: list[RowEvaluation], c
|
|
|
180
261
|
"Content-Type": "application/json",
|
|
181
262
|
},
|
|
182
263
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
264
|
+
data = _read_json_response_with_retries(
|
|
265
|
+
request,
|
|
266
|
+
timeout_seconds=config.reflection_http_timeout_seconds,
|
|
267
|
+
retry_attempts=config.reflection_retry_attempts,
|
|
268
|
+
failure_prefix="OpenAI reflection LM failed",
|
|
269
|
+
)
|
|
189
270
|
text = _extract_openai_output_text(data)
|
|
190
271
|
if not text:
|
|
191
272
|
raise RuntimeError("OpenAI reflection LM returned no output text")
|
|
@@ -22,15 +22,18 @@ Skip this skill when:
|
|
|
22
22
|
|
|
23
23
|
- Node.js 20+ installed.
|
|
24
24
|
- Install the CLI: `npm i -g orizu`.
|
|
25
|
-
- Install this skill from the CLI package: `orizu install-skill --
|
|
25
|
+
- Install this skill from the CLI package: `orizu install-skill --agent codex --yes` (or `--agent claude`; advanced target IDs remain available via `--target`).
|
|
26
|
+
- Or read the bundled skill in place without installing: `orizu skills path --json` returns `root`, `skillMd`, `source`, `cliVersion`, and `skillHash`.
|
|
27
|
+
- After CLI upgrades, check installed copies with `orizu skills status` and refresh them with `orizu skills update`.
|
|
26
28
|
- Rediscover the command surface any time with `orizu --help`, `orizu <group> --help`, or `orizu capabilities --json`.
|
|
29
|
+
- Every command supports `--json` (prefix `orizu --json <cmd>` or trailing flag) and emits a single JSON document; prefer it over parsing human-formatted text. Long-running commands emit the JSON summary as the final stdout line.
|
|
27
30
|
- Login callback requires `127.0.0.1:43123`.
|
|
28
31
|
- Credentials are stored at `~/.config/orizu/credentials.json`.
|
|
29
32
|
- Current `orizu login` creates a user-owned personal access token for the CLI and stores it as a v3 API-key credential. The raw token is not recoverable after creation.
|
|
30
33
|
|
|
31
34
|
## Login
|
|
32
35
|
|
|
33
|
-
- Verify auth: `orizu whoami
|
|
36
|
+
- Verify auth: `orizu whoami --json` (returns `{user, server}`; non-zero exit means sign in is needed).
|
|
34
37
|
- Start the login flow: `orizu login` (opens a browser tab).
|
|
35
38
|
- Approving login in the browser creates a personal access token for that Orizu user. The token inherits the user's current team/project access and loses access if the user's role or membership changes.
|
|
36
39
|
- Clear local credentials and revoke the current CLI token: `orizu logout`.
|
|
@@ -86,6 +89,8 @@ orizu tasks create --project <teamSlug>/<projectSlug> --dataset <datasetId> --ap
|
|
|
86
89
|
|
|
87
90
|
# Open the returned task URL and test the draft manually before assigning.
|
|
88
91
|
orizu tasks publish --task <taskId> --assignees <userId1,userId2>
|
|
92
|
+
# Or publish exact row assignments:
|
|
93
|
+
orizu tasks publish --task <taskId> --assignment-file ./assignments.jsonl
|
|
89
94
|
|
|
90
95
|
orizu tasks status --task <taskId>
|
|
91
96
|
orizu tasks report set --task <taskId> --report-file ./task-report.md
|
|
@@ -143,12 +148,14 @@ Local execution workflow:
|
|
|
143
148
|
7. Diff before/after on a held-out set; ship if it holds.
|
|
144
149
|
|
|
145
150
|
Bundled `run-gepa` reflection behavior:
|
|
151
|
+
- Budget controls are mutually exclusive. Use at most one of `--budget auto|light|medium|heavy`, `--max-metric-calls <n>`, `--max-full-evals <n>`, or `--max-iterations <n>`. If none is provided, `run-gepa` uses `--budget auto`, the balanced medium preset.
|
|
146
152
|
- The reflective LM's final text is used verbatim as the next candidate prompt. It should return only the complete updated prompt body, not analysis, labels, XML tags, or markdown fences.
|
|
147
153
|
- Keep provider-native reasoning controls separate from the prompt text with `--reflection-provider-settings <json|@file>`.
|
|
148
154
|
- Reflection max-token limits are explicit config, not implicit defaults. Use `--reflection-max-tokens <n>` when a provider requires an output cap; this maps to Anthropic `max_tokens` and OpenAI `max_output_tokens`.
|
|
149
155
|
- Anthropic native Messages reflection requires `max_tokens`, so `run-gepa` with an `anthropic/...` reflection model must receive `--reflection-max-tokens <n>` and should fail fast if it is omitted. OpenAI reflection may omit `max_output_tokens` unless the user explicitly wants a cap.
|
|
156
|
+
- Reflection HTTP calls retry transient provider failures by default (`--reflection-retry-attempts 3`, `--reflection-http-timeout-seconds 180`). Retryable failures include timeouts, connection reset/abort, HTTP 429, and common server/capacity statuses. If reflection still fails after retries, `run-gepa` logs `reflection_failed`, counts the proposal against budget, and continues to the next iteration.
|
|
150
157
|
- By default, `run-gepa` skips reflection and child creation when every row in the selected parent mini-batch is already perfect (`1.0` for higher-is-better scorers, `0.0` for lower-is-better scorers). This saves reflection tokens because the child cannot beat a saturated mini-batch. Use `--no-skip-perfect-parent-reflection` to force reflection anyway, or `--skip-perfect-parent-reflection` to make the default explicit in scripted runs.
|
|
151
|
-
- By default, `run-gepa` sets `--num-threads auto` for row evaluations. Auto uses mini-batch size, validation-set size, CPU count, memory estimate, file-descriptor limit, and
|
|
158
|
+
- By default, `run-gepa` sets `--num-threads auto` for row evaluations. Auto uses mini-batch size, validation-set size, 2x CPU count, memory estimate, file-descriptor limit, and a 64-thread default ceiling to avoid launching too many runner subprocesses at once while still scaling large runs. Set `ORIZU_GEPA_AUTO_THREADS_MAX` or use `--num-threads <n>` only when the runner/provider capacity is known.
|
|
152
159
|
- `run-gepa` writes complete local logs by default under `logs/<optimization_run_id>`; override with `--log-dir <dir>` or disable with `--no-local-log`.
|
|
153
160
|
- Server optimization events redact row snapshots and reflection prompts by default; the local log keeps full row inputs, outputs, scores, feedback, reflection prompts, and reflection responses for later agent analysis.
|
|
154
161
|
- OpenAI example: `--reflection-model openai/gpt-5 --reflection-provider-settings '{"reasoning":{"effort":"medium","summary":"auto"}}'`.
|
|
@@ -170,9 +177,9 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
|
|
|
170
177
|
# Execution rules
|
|
171
178
|
|
|
172
179
|
- Prefer explicit flags in non-TTY contexts; reserve interactive fallback for TTY.
|
|
173
|
-
- Canonical selectors: `--team`, `--project`, `--app`, `--task`, `--dataset`, `--assignees`.
|
|
174
|
-
- `tasks create` creates drafts by default. Test the returned task URL manually, then use `tasks publish --task <taskId> --assignees <userId1,userId2>` after approval.
|
|
175
|
-
- `tasks assign` and `tasks publish` take user IDs, not emails.
|
|
180
|
+
- Canonical selectors: `--team`, `--project`, `--app`, `--task`, `--dataset`, `--assignees`, `--assignment-file`.
|
|
181
|
+
- `tasks create` creates drafts by default. Test the returned task URL manually, then use `tasks publish --task <taskId> --assignees <userId1,userId2>` or `tasks publish --task <taskId> --assignment-file <path>` after approval.
|
|
182
|
+
- `tasks assign --assignees` and `tasks publish --assignees` take user IDs, not emails. `--assignment-file` accepts emails or user IDs in JSONL rows shaped as `{ "rowId": "...", "assignee": "..." }` or `{ "rowId": "...", "assignees": ["..."] }`.
|
|
176
183
|
- `datasets edit-rows` requires a non-empty string `id` on each row in `--file`.
|
|
177
184
|
- `datasets delete-rows` uses `--row-ids` as the canonical selector.
|
|
178
185
|
- `datasets delete` requires interactive terminal confirmation; there is no non-interactive confirmation option.
|