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.
@@ -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)
@@ -19,6 +22,12 @@ def positive_int(value: str) -> int:
19
22
  return parsed
20
23
 
21
24
 
25
+ def num_threads_arg(value: str) -> int | str:
26
+ if value.strip().lower() == "auto":
27
+ return "auto"
28
+ return positive_int(value)
29
+
30
+
22
31
  def read_text_arg(value: str | None) -> str | None:
23
32
  if value is None:
24
33
  return None
@@ -40,6 +49,38 @@ def read_json_object_arg(value: str | None, flag_name: str) -> dict[str, Any] |
40
49
  return parsed
41
50
 
42
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
+
43
84
  def main() -> None:
44
85
  parser = argparse.ArgumentParser(prog="orizu-gepa")
45
86
  parser.add_argument("--project", required=True)
@@ -54,9 +95,10 @@ def main() -> None:
54
95
  parser.add_argument("--split-set-id", required=True)
55
96
  parser.add_argument("--train-split", default="train")
56
97
  parser.add_argument("--val-split", default="validation")
57
- parser.add_argument("--budget", default="light", choices=["auto", "light", "medium", "high"])
58
- parser.add_argument("--max-iterations", type=positive_int, default=3)
98
+ parser.add_argument("--budget", choices=["auto", "light", "medium", "heavy"])
99
+ parser.add_argument("--max-iterations", type=positive_int)
59
100
  parser.add_argument("--minibatch-size", type=positive_int, default=3)
101
+ parser.add_argument("--num-threads", type=num_threads_arg, default=TextGepaConfig.num_threads)
60
102
  parser.add_argument(
61
103
  "--candidate-selection-strategy",
62
104
  default=TextGepaConfig.candidate_selection_strategy,
@@ -69,6 +111,18 @@ def main() -> None:
69
111
  parser.add_argument("--reflection-model", default=TextGepaConfig.reflection_model)
70
112
  parser.add_argument("--reflection-temperature", type=float)
71
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
+ )
72
126
  parser.add_argument("--reflection-prompt-template")
73
127
  parser.add_argument("--reflection-provider-settings")
74
128
  parser.add_argument(
@@ -92,7 +146,12 @@ def main() -> None:
92
146
  parser.add_argument("--log-dir", default="logs")
93
147
  parser.add_argument("--no-local-log", action="store_true")
94
148
  parser.add_argument("--metadata", default="{}")
95
- args = parser.parse_args()
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))
96
155
 
97
156
  metadata = json.loads(args.metadata)
98
157
  if not isinstance(metadata, dict):
@@ -102,6 +161,7 @@ def main() -> None:
102
161
  budget=args.budget,
103
162
  max_iterations=args.max_iterations,
104
163
  minibatch_size=args.minibatch_size,
164
+ num_threads=args.num_threads,
105
165
  candidate_selection_strategy=args.candidate_selection_strategy,
106
166
  epsilon=args.epsilon,
107
167
  max_metric_calls=args.max_metric_calls,
@@ -109,6 +169,8 @@ def main() -> None:
109
169
  reflection_model=args.reflection_model,
110
170
  reflection_temperature=args.reflection_temperature,
111
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,
112
174
  reflection_prompt_template=read_text_arg(args.reflection_prompt_template),
113
175
  reflection_provider_settings=read_json_object_arg(args.reflection_provider_settings, "--reflection-provider-settings") or {},
114
176
  skip_perfect_parent_reflection=args.skip_perfect_parent_reflection,
@@ -153,6 +215,7 @@ def main() -> None:
153
215
  "dataset_size": len(trainset) + len(valset),
154
216
  "train_count": len(trainset),
155
217
  "validation_count": len(valset),
218
+ "num_threads": args.num_threads,
156
219
  }
157
220
 
158
221
  run_id = client.start_run(
@@ -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(f"[orizu-gepa] {self.sequence:03d} {event_type}", flush=True)
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