orizu 0.3.2 → 0.3.4
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 +15 -0
- package/dist/index.js +281 -14
- package/package.json +1 -1
- package/src/index.ts +427 -14
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +46 -13
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +46 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +123 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +46 -1
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from .client import OrizuClient, OrizuEventSink
|
|
9
|
+
from .local_log import LocalOptimizationLogger
|
|
9
10
|
from .optimizer import TextGepaConfig, optimize_loaded_text_candidate
|
|
10
11
|
from .reflection import reflect_with_provider
|
|
11
12
|
from .runner import make_candidate_runner, make_scorer_runner
|
|
@@ -74,6 +75,8 @@ def main() -> None:
|
|
|
74
75
|
parser.add_argument("--auto-promote", action="store_true")
|
|
75
76
|
parser.add_argument("--promotion-label")
|
|
76
77
|
parser.add_argument("--log-row-snapshots", action="store_true")
|
|
78
|
+
parser.add_argument("--log-dir", default="logs")
|
|
79
|
+
parser.add_argument("--no-local-log", action="store_true")
|
|
77
80
|
parser.add_argument("--metadata", default="{}")
|
|
78
81
|
args = parser.parse_args()
|
|
79
82
|
|
|
@@ -123,6 +126,19 @@ def main() -> None:
|
|
|
123
126
|
split=args.val_split,
|
|
124
127
|
)
|
|
125
128
|
|
|
129
|
+
run_metadata = {
|
|
130
|
+
**metadata,
|
|
131
|
+
"optimizer_package": "orizu-gepa-python",
|
|
132
|
+
"optimizer_family": "gepa",
|
|
133
|
+
"mode": "text-candidate",
|
|
134
|
+
"inference_lm": prompt_context.provider_settings.get("model"),
|
|
135
|
+
"reflection_lm": config.reflection_model,
|
|
136
|
+
"scorer_lm": scorer_context.provider_settings.get("model"),
|
|
137
|
+
"dataset_size": len(trainset) + len(valset),
|
|
138
|
+
"train_count": len(trainset),
|
|
139
|
+
"validation_count": len(valset),
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
run_id = client.start_run(
|
|
127
143
|
project=args.project,
|
|
128
144
|
optimizer_version_id=args.optimizer_version_id,
|
|
@@ -132,21 +148,29 @@ def main() -> None:
|
|
|
132
148
|
split_set_id=args.split_set_id,
|
|
133
149
|
train_split=args.train_split,
|
|
134
150
|
validation_split=args.val_split,
|
|
135
|
-
metadata=
|
|
136
|
-
**metadata,
|
|
137
|
-
"optimizer_package": "orizu-gepa-python",
|
|
138
|
-
"optimizer_family": "gepa",
|
|
139
|
-
"mode": "text-candidate",
|
|
140
|
-
"inference_lm": prompt_context.provider_settings.get("model"),
|
|
141
|
-
"reflection_lm": config.reflection_model,
|
|
142
|
-
"scorer_lm": scorer_context.provider_settings.get("model"),
|
|
143
|
-
"dataset_size": len(trainset) + len(valset),
|
|
144
|
-
"train_count": len(trainset),
|
|
145
|
-
"validation_count": len(valset),
|
|
146
|
-
},
|
|
151
|
+
metadata=run_metadata,
|
|
147
152
|
)
|
|
148
153
|
print(f"[orizu-gepa] started optimization run {run_id}", flush=True)
|
|
149
154
|
|
|
155
|
+
local_logger = None
|
|
156
|
+
if not args.no_local_log:
|
|
157
|
+
local_logger = LocalOptimizationLogger.create(args.log_dir, run_id)
|
|
158
|
+
local_logger.write_context(
|
|
159
|
+
project=args.project,
|
|
160
|
+
run_id=run_id,
|
|
161
|
+
args={
|
|
162
|
+
key: value
|
|
163
|
+
for key, value in vars(args).items()
|
|
164
|
+
if key not in {"metadata"}
|
|
165
|
+
},
|
|
166
|
+
prompt_context=prompt_context,
|
|
167
|
+
scorer_context=scorer_context,
|
|
168
|
+
trainset=trainset,
|
|
169
|
+
valset=valset,
|
|
170
|
+
metadata=run_metadata,
|
|
171
|
+
)
|
|
172
|
+
print(f"[orizu-gepa] local log: {local_logger.path}", flush=True)
|
|
173
|
+
|
|
150
174
|
result = optimize_loaded_text_candidate(
|
|
151
175
|
run_id=run_id,
|
|
152
176
|
prompt_context=prompt_context,
|
|
@@ -156,9 +180,17 @@ def main() -> None:
|
|
|
156
180
|
candidate_runner=make_candidate_runner(Path(args.candidate_runner_dir), run_id),
|
|
157
181
|
scorer_runner=make_scorer_runner(Path(args.scorer_runner_dir), run_id),
|
|
158
182
|
reflector=reflect_with_provider,
|
|
159
|
-
event_sink=OrizuEventSink(
|
|
183
|
+
event_sink=OrizuEventSink(
|
|
184
|
+
client,
|
|
185
|
+
run_id,
|
|
186
|
+
fail_on_log_error=config.fail_on_log_error,
|
|
187
|
+
local_logger=local_logger,
|
|
188
|
+
),
|
|
160
189
|
config=config,
|
|
190
|
+
local_logger=local_logger,
|
|
161
191
|
)
|
|
192
|
+
if local_logger is not None:
|
|
193
|
+
local_logger.write_result(result)
|
|
162
194
|
print(json.dumps({
|
|
163
195
|
"optimization_run_id": result.run_id,
|
|
164
196
|
"best_candidate_id": result.best_candidate_id,
|
|
@@ -166,6 +198,7 @@ def main() -> None:
|
|
|
166
198
|
"seed_score": result.seed_score,
|
|
167
199
|
"promoted_prompt_version_id": result.promoted_prompt_version_id,
|
|
168
200
|
"budget": result.budget.to_payload(),
|
|
201
|
+
"local_log_dir": local_logger.path if local_logger is not None else None,
|
|
169
202
|
}))
|
|
170
203
|
|
|
171
204
|
|
|
@@ -255,12 +255,14 @@ class OrizuEventSink:
|
|
|
255
255
|
*,
|
|
256
256
|
fail_on_log_error: bool = True,
|
|
257
257
|
max_log_retries: int = 2,
|
|
258
|
+
local_logger: Any | None = None,
|
|
258
259
|
):
|
|
259
260
|
self.client = client
|
|
260
261
|
self.run_id = run_id
|
|
261
262
|
self.sequence = 0
|
|
262
263
|
self.fail_on_log_error = fail_on_log_error
|
|
263
264
|
self.max_log_retries = max(0, max_log_retries)
|
|
265
|
+
self.local_logger = local_logger
|
|
264
266
|
|
|
265
267
|
def log_event(
|
|
266
268
|
self,
|
|
@@ -275,6 +277,20 @@ class OrizuEventSink:
|
|
|
275
277
|
child_candidate_id: str | None = None,
|
|
276
278
|
) -> None:
|
|
277
279
|
self.sequence += 1
|
|
280
|
+
local_event = {
|
|
281
|
+
"run_id": self.run_id,
|
|
282
|
+
"sequence": self.sequence,
|
|
283
|
+
"event_type": event_type,
|
|
284
|
+
"event_layer": event_layer,
|
|
285
|
+
"optimizer_family": optimizer_family,
|
|
286
|
+
"iteration": iteration,
|
|
287
|
+
"candidate_id": candidate_id,
|
|
288
|
+
"parent_candidate_id": parent_candidate_id,
|
|
289
|
+
"child_candidate_id": child_candidate_id,
|
|
290
|
+
"payload": payload or {},
|
|
291
|
+
}
|
|
292
|
+
if self.local_logger is not None:
|
|
293
|
+
self.local_logger.append_event(local_event)
|
|
278
294
|
last_error: Exception | None = None
|
|
279
295
|
for attempt in range(self.max_log_retries + 1):
|
|
280
296
|
try:
|
|
@@ -308,7 +324,37 @@ class OrizuEventSink:
|
|
|
308
324
|
prompt_version_id = self.client.promote_candidate(self.run_id, **kwargs)
|
|
309
325
|
# Promotion writes a system event at max(sequence)+1 in Postgres.
|
|
310
326
|
self.sequence += 1
|
|
327
|
+
if self.local_logger is not None:
|
|
328
|
+
self.local_logger.append_event({
|
|
329
|
+
"run_id": self.run_id,
|
|
330
|
+
"sequence": self.sequence,
|
|
331
|
+
"event_type": "candidate_promoted",
|
|
332
|
+
"event_layer": "system",
|
|
333
|
+
"optimizer_family": "gepa",
|
|
334
|
+
"iteration": None,
|
|
335
|
+
"candidate_id": kwargs.get("candidate_id"),
|
|
336
|
+
"parent_candidate_id": None,
|
|
337
|
+
"child_candidate_id": None,
|
|
338
|
+
"payload": {
|
|
339
|
+
**kwargs,
|
|
340
|
+
"prompt_version_id": prompt_version_id,
|
|
341
|
+
},
|
|
342
|
+
})
|
|
311
343
|
return prompt_version_id
|
|
312
344
|
|
|
313
345
|
def finish_run(self, **kwargs: Any) -> None:
|
|
346
|
+
if self.local_logger is not None:
|
|
347
|
+
self.sequence += 1
|
|
348
|
+
self.local_logger.append_event({
|
|
349
|
+
"run_id": self.run_id,
|
|
350
|
+
"sequence": self.sequence,
|
|
351
|
+
"event_type": "run_status_updated",
|
|
352
|
+
"event_layer": "system",
|
|
353
|
+
"optimizer_family": "gepa",
|
|
354
|
+
"iteration": None,
|
|
355
|
+
"candidate_id": kwargs.get("best_candidate_id"),
|
|
356
|
+
"parent_candidate_id": None,
|
|
357
|
+
"child_candidate_id": None,
|
|
358
|
+
"payload": kwargs,
|
|
359
|
+
})
|
|
314
360
|
self.client.update_run(self.run_id, **kwargs)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .optimizer import DatasetRow, PromptContext, RowEvaluation, TextGepaResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _json_default(value: Any) -> Any:
|
|
12
|
+
if dataclasses.is_dataclass(value):
|
|
13
|
+
return dataclasses.asdict(value)
|
|
14
|
+
return str(value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
18
|
+
path.write_text(
|
|
19
|
+
json.dumps(payload, indent=2, ensure_ascii=False, default=_json_default) + "\n",
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _append_jsonl(path: Path, payload: dict[str, Any]) -> None:
|
|
25
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
26
|
+
handle.write(json.dumps(payload, ensure_ascii=False, default=_json_default) + "\n")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LocalOptimizationLogger:
|
|
30
|
+
def __init__(self, directory: Path):
|
|
31
|
+
self.directory = directory
|
|
32
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def create(cls, root: str | Path, run_id: str) -> "LocalOptimizationLogger":
|
|
36
|
+
return cls(Path(root) / run_id)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def path(self) -> str:
|
|
40
|
+
return str(self.directory)
|
|
41
|
+
|
|
42
|
+
def write_context(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
project: str,
|
|
46
|
+
run_id: str,
|
|
47
|
+
args: dict[str, Any],
|
|
48
|
+
prompt_context: PromptContext,
|
|
49
|
+
scorer_context: PromptContext,
|
|
50
|
+
trainset: list[DatasetRow],
|
|
51
|
+
valset: list[DatasetRow],
|
|
52
|
+
metadata: dict[str, Any],
|
|
53
|
+
) -> None:
|
|
54
|
+
_write_json(self.directory / "run.json", {
|
|
55
|
+
"schema_version": "orizu.optimization-local-log.v1",
|
|
56
|
+
"optimization_run_id": run_id,
|
|
57
|
+
"project": project,
|
|
58
|
+
"args": args,
|
|
59
|
+
"metadata": metadata,
|
|
60
|
+
})
|
|
61
|
+
_write_json(self.directory / "prompt_context.json", dataclasses.asdict(prompt_context))
|
|
62
|
+
_write_json(self.directory / "scorer_context.json", dataclasses.asdict(scorer_context))
|
|
63
|
+
_write_json(self.directory / "trainset.json", {
|
|
64
|
+
"rows": [dataclasses.asdict(row) for row in trainset],
|
|
65
|
+
})
|
|
66
|
+
_write_json(self.directory / "valset.json", {
|
|
67
|
+
"rows": [dataclasses.asdict(row) for row in valset],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
def append_event(self, event: dict[str, Any]) -> None:
|
|
71
|
+
_append_jsonl(self.directory / "events.jsonl", event)
|
|
72
|
+
|
|
73
|
+
def append_evaluations(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
stage: str,
|
|
77
|
+
split: str,
|
|
78
|
+
iteration: int | None,
|
|
79
|
+
candidate_id: str,
|
|
80
|
+
parent_candidate_id: str | None = None,
|
|
81
|
+
results: list[RowEvaluation],
|
|
82
|
+
) -> None:
|
|
83
|
+
for result in results:
|
|
84
|
+
_append_jsonl(self.directory / "evaluations.jsonl", {
|
|
85
|
+
"stage": stage,
|
|
86
|
+
"split": split,
|
|
87
|
+
"iteration": iteration,
|
|
88
|
+
"candidate_id": candidate_id,
|
|
89
|
+
"parent_candidate_id": parent_candidate_id,
|
|
90
|
+
**result.to_payload(),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
def append_reflection(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
iteration: int,
|
|
97
|
+
parent_candidate_id: str,
|
|
98
|
+
child_candidate_id: str,
|
|
99
|
+
row_ids: list[str],
|
|
100
|
+
prompt: str,
|
|
101
|
+
response: str,
|
|
102
|
+
candidate_text: str,
|
|
103
|
+
) -> None:
|
|
104
|
+
_append_jsonl(self.directory / "reflections.jsonl", {
|
|
105
|
+
"iteration": iteration,
|
|
106
|
+
"parent_candidate_id": parent_candidate_id,
|
|
107
|
+
"child_candidate_id": child_candidate_id,
|
|
108
|
+
"row_ids": row_ids,
|
|
109
|
+
"prompt": prompt,
|
|
110
|
+
"response": response,
|
|
111
|
+
"candidate_text": candidate_text,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
def write_result(self, result: TextGepaResult) -> None:
|
|
115
|
+
_write_json(self.directory / "result.json", {
|
|
116
|
+
"optimization_run_id": result.run_id,
|
|
117
|
+
"best_candidate_id": result.best_candidate_id,
|
|
118
|
+
"best_candidate_text": result.best_candidate_text,
|
|
119
|
+
"best_score": result.best_score,
|
|
120
|
+
"seed_score": result.seed_score,
|
|
121
|
+
"promoted_prompt_version_id": result.promoted_prompt_version_id,
|
|
122
|
+
"budget": result.budget.to_payload(),
|
|
123
|
+
})
|
|
@@ -658,6 +658,7 @@ def optimize_loaded_text_candidate(
|
|
|
658
658
|
reflector: Reflector,
|
|
659
659
|
event_sink: EventSink,
|
|
660
660
|
config: TextGepaConfig,
|
|
661
|
+
local_logger: Any | None = None,
|
|
661
662
|
) -> TextGepaResult:
|
|
662
663
|
seed_text = prompt_context.body or ""
|
|
663
664
|
budget = Budget.from_config(config)
|
|
@@ -709,6 +710,14 @@ def optimize_loaded_text_candidate(
|
|
|
709
710
|
budget=budget,
|
|
710
711
|
evaluation_cache=evaluation_cache,
|
|
711
712
|
)
|
|
713
|
+
if local_logger is not None:
|
|
714
|
+
local_logger.append_evaluations(
|
|
715
|
+
stage="seed_val_set",
|
|
716
|
+
split="validation",
|
|
717
|
+
iteration=None,
|
|
718
|
+
candidate_id="seed",
|
|
719
|
+
results=seed_results,
|
|
720
|
+
)
|
|
712
721
|
if not _completed_all_rows(seed_results, valset):
|
|
713
722
|
raise RuntimeError("Budget exhausted before seed validation completed")
|
|
714
723
|
budget.used_full_evals += 1
|
|
@@ -804,6 +813,14 @@ def optimize_loaded_text_candidate(
|
|
|
804
813
|
budget=budget,
|
|
805
814
|
evaluation_cache=evaluation_cache,
|
|
806
815
|
)
|
|
816
|
+
if local_logger is not None:
|
|
817
|
+
local_logger.append_evaluations(
|
|
818
|
+
stage="parent_minibatch",
|
|
819
|
+
split="train",
|
|
820
|
+
iteration=iteration,
|
|
821
|
+
candidate_id=parent_candidate_id,
|
|
822
|
+
results=parent_results,
|
|
823
|
+
)
|
|
807
824
|
if not _completed_all_rows(parent_results, minibatch):
|
|
808
825
|
event_sink.log_event(
|
|
809
826
|
"budget_exhausted",
|
|
@@ -852,11 +869,21 @@ def optimize_loaded_text_candidate(
|
|
|
852
869
|
child_text = reflection.candidate_text
|
|
853
870
|
candidate_text_by_id[child_id] = child_text
|
|
854
871
|
budget.used_candidate_proposals += 1
|
|
872
|
+
if local_logger is not None:
|
|
873
|
+
local_logger.append_reflection(
|
|
874
|
+
iteration=iteration,
|
|
875
|
+
parent_candidate_id=parent_candidate_id,
|
|
876
|
+
child_candidate_id=child_id,
|
|
877
|
+
row_ids=[row.id for row in minibatch],
|
|
878
|
+
prompt=reflection.prompt,
|
|
879
|
+
response=reflection.response,
|
|
880
|
+
candidate_text=reflection.candidate_text,
|
|
881
|
+
)
|
|
855
882
|
event_sink.log_event(
|
|
856
883
|
"reflection_completed",
|
|
857
884
|
{
|
|
858
885
|
**_text_log_fields("prompt", reflection.prompt, include_text=config.log_row_snapshots),
|
|
859
|
-
|
|
886
|
+
"response": reflection.response,
|
|
860
887
|
"child_candidate_id": child_id,
|
|
861
888
|
"candidate_text": child_text,
|
|
862
889
|
},
|
|
@@ -908,6 +935,15 @@ def optimize_loaded_text_candidate(
|
|
|
908
935
|
budget=budget,
|
|
909
936
|
evaluation_cache=evaluation_cache,
|
|
910
937
|
)
|
|
938
|
+
if local_logger is not None:
|
|
939
|
+
local_logger.append_evaluations(
|
|
940
|
+
stage="child_minibatch",
|
|
941
|
+
split="train",
|
|
942
|
+
iteration=iteration,
|
|
943
|
+
candidate_id=child_id,
|
|
944
|
+
parent_candidate_id=parent_candidate_id,
|
|
945
|
+
results=child_results,
|
|
946
|
+
)
|
|
911
947
|
if not _completed_all_rows(child_results, minibatch):
|
|
912
948
|
event_sink.log_event(
|
|
913
949
|
"budget_exhausted",
|
|
@@ -981,6 +1017,15 @@ def optimize_loaded_text_candidate(
|
|
|
981
1017
|
budget=budget,
|
|
982
1018
|
evaluation_cache=evaluation_cache,
|
|
983
1019
|
)
|
|
1020
|
+
if local_logger is not None:
|
|
1021
|
+
local_logger.append_evaluations(
|
|
1022
|
+
stage="child_val_set",
|
|
1023
|
+
split="validation",
|
|
1024
|
+
iteration=iteration,
|
|
1025
|
+
candidate_id=child_id,
|
|
1026
|
+
parent_candidate_id=parent_candidate_id,
|
|
1027
|
+
results=child_val_results,
|
|
1028
|
+
)
|
|
984
1029
|
if not _completed_all_rows(child_val_results, valset):
|
|
985
1030
|
event_sink.log_event(
|
|
986
1031
|
"budget_exhausted",
|