orizu 0.3.4 → 0.3.6
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 +40 -3
- package/dist/help.js +611 -0
- package/dist/index.js +458 -20
- package/dist/preview-runtime.js +857 -0
- package/dist/skill-installer.js +180 -0
- package/package.json +12 -3
- package/scripts/vendor-orizu-skill.mjs +54 -0
- package/src/help.ts +676 -0
- package/src/index.ts +568 -21
- package/src/preview-runtime.ts +976 -0
- package/src/skill-installer.ts +260 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +16 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +68 -56
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +31 -6
- package/vendor/skills/orizu-cli/SKILL.md +185 -0
- package/vendor/skills/orizu-cli/agents/openai.yaml +4 -0
- package/vendor/skills/orizu-cli/references/building-apps.md +1461 -0
- package/vendor/skills/orizu-cli/references/building-judges.md +218 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +324 -0
- package/vendor/skills/orizu-cli/references/optimization-reports.md +174 -0
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +291 -0
- package/vendor/skills/orizu-cli/references/primer.md +245 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +830 -0
- package/vendor/skills/orizu-cli/scripts/test-app.mjs +492 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'fs'
|
|
9
|
+
import { dirname, isAbsolute, relative, resolve } from 'path'
|
|
10
|
+
import { fileURLToPath } from 'url'
|
|
11
|
+
import { homedir } from 'os'
|
|
12
|
+
|
|
13
|
+
export const SKILL_NAME = 'orizu-cli'
|
|
14
|
+
export const AGENTS_START_MARKER = '<!-- orizu-cli:start -->'
|
|
15
|
+
export const AGENTS_END_MARKER = '<!-- orizu-cli:end -->'
|
|
16
|
+
|
|
17
|
+
export const SKILL_INSTALL_TARGETS = [
|
|
18
|
+
'agent-user',
|
|
19
|
+
'codex-project',
|
|
20
|
+
'claude-user',
|
|
21
|
+
'claude-project',
|
|
22
|
+
'agents-md',
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
export type SkillInstallTarget = typeof SKILL_INSTALL_TARGETS[number]
|
|
26
|
+
|
|
27
|
+
export interface SkillInstallOptions {
|
|
28
|
+
cwd?: string
|
|
29
|
+
homeDir?: string
|
|
30
|
+
overwrite?: boolean
|
|
31
|
+
dryRun?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SkillInstallResult {
|
|
35
|
+
target: SkillInstallTarget
|
|
36
|
+
path: string
|
|
37
|
+
action: 'created' | 'updated' | 'would-create' | 'would-update'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCliRoot(): string {
|
|
41
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSkillDir(path: string): boolean {
|
|
45
|
+
return existsSync(resolve(path, 'SKILL.md'))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isSafeRelativeSkillPath(rel: string): boolean {
|
|
49
|
+
return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatMissingSkillSourceError(paths: string[]): string {
|
|
53
|
+
return `Unable to find bundled ${SKILL_NAME} skill. Reinstall the orizu CLI or run from the Orizu repository.\n`
|
|
54
|
+
+ `Searched:\n- ${paths.join('\n- ')}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveSkillSourceDir(): string {
|
|
58
|
+
const override = process.env.ORIZU_SKILL_SOURCE_DIR
|
|
59
|
+
if (override) {
|
|
60
|
+
const resolvedOverride = resolve(override)
|
|
61
|
+
if (!isSkillDir(resolvedOverride)) {
|
|
62
|
+
throw new Error(`ORIZU_SKILL_SOURCE_DIR is not an Orizu skill directory: ${resolvedOverride}`)
|
|
63
|
+
}
|
|
64
|
+
return resolvedOverride
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cliRoot = getCliRoot()
|
|
68
|
+
const packagedSkill = resolve(cliRoot, 'vendor', 'skills', SKILL_NAME)
|
|
69
|
+
if (isSkillDir(packagedSkill)) {
|
|
70
|
+
return packagedSkill
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const repoSkill = resolve(cliRoot, '..', '..', 'skills', SKILL_NAME)
|
|
74
|
+
if (isSkillDir(repoSkill)) {
|
|
75
|
+
return repoSkill
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const searchedPaths = [
|
|
79
|
+
`packaged skill: ${packagedSkill}`,
|
|
80
|
+
`repository fallback: ${repoSkill}`,
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
throw new Error(formatMissingSkillSourceError(searchedPaths))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveHome(options?: SkillInstallOptions): string {
|
|
87
|
+
return resolve(options?.homeDir || process.env.HOME || homedir())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveCwd(options?: SkillInstallOptions): string {
|
|
91
|
+
return resolve(options?.cwd || process.cwd())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getSkillInstallPath(
|
|
95
|
+
target: SkillInstallTarget,
|
|
96
|
+
options?: SkillInstallOptions
|
|
97
|
+
): string {
|
|
98
|
+
const home = resolveHome(options)
|
|
99
|
+
const cwd = resolveCwd(options)
|
|
100
|
+
|
|
101
|
+
if (target === 'agent-user') {
|
|
102
|
+
return resolve(home, '.agents', 'skills', SKILL_NAME)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (target === 'codex-project') {
|
|
106
|
+
return resolve(cwd, '.codex', 'skills', SKILL_NAME)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (target === 'claude-user') {
|
|
110
|
+
return resolve(home, '.claude', 'skills', SKILL_NAME)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (target === 'claude-project') {
|
|
114
|
+
return resolve(cwd, '.claude', 'skills', SKILL_NAME)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return resolve(cwd, 'AGENTS.md')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shouldCopySkillPath(sourceRoot: string, sourcePath: string): boolean {
|
|
121
|
+
const rel = relative(sourceRoot, sourcePath).replace(/\\/g, '/')
|
|
122
|
+
if (!rel) {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
if (!isSafeRelativeSkillPath(rel)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parts = rel.split('/')
|
|
130
|
+
return !parts.some(part =>
|
|
131
|
+
part === '.DS_Store' ||
|
|
132
|
+
part === '__pycache__' ||
|
|
133
|
+
part === '.pytest_cache'
|
|
134
|
+
) && !rel.endsWith('.pyc') && !rel.endsWith('.pyo')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function copySkillTree(sourceDir: string, targetDir: string) {
|
|
138
|
+
rmSync(targetDir, { recursive: true, force: true })
|
|
139
|
+
mkdirSync(dirname(targetDir), { recursive: true })
|
|
140
|
+
cpSync(sourceDir, targetDir, {
|
|
141
|
+
recursive: true,
|
|
142
|
+
filter: sourcePath => shouldCopySkillPath(sourceDir, sourcePath),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stripFrontmatter(markdown: string): string {
|
|
147
|
+
const match = markdown.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/)
|
|
148
|
+
if (!match) {
|
|
149
|
+
return markdown
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return markdown.slice(match[0].length).replace(/^[\r\n]+/, '')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildAgentsSection(sourceDir: string): string {
|
|
156
|
+
const skillMarkdown = readFileSync(resolve(sourceDir, 'SKILL.md'), 'utf8')
|
|
157
|
+
const body = stripFrontmatter(skillMarkdown).trimEnd()
|
|
158
|
+
return `${AGENTS_START_MARKER}\n${body}\n${AGENTS_END_MARKER}\n`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function installAgentsMd(
|
|
162
|
+
sourceDir: string,
|
|
163
|
+
targetPath: string,
|
|
164
|
+
options?: SkillInstallOptions
|
|
165
|
+
): SkillInstallResult {
|
|
166
|
+
const section = buildAgentsSection(sourceDir)
|
|
167
|
+
const exists = existsSync(targetPath)
|
|
168
|
+
const existing = exists ? readFileSync(targetPath, 'utf8') : ''
|
|
169
|
+
const hasManagedSection = existing.includes(AGENTS_START_MARKER)
|
|
170
|
+
&& existing.includes(AGENTS_END_MARKER)
|
|
171
|
+
const action = exists ? 'updated' : 'created'
|
|
172
|
+
|
|
173
|
+
if (options?.dryRun) {
|
|
174
|
+
return {
|
|
175
|
+
target: 'agents-md',
|
|
176
|
+
path: targetPath,
|
|
177
|
+
action: action === 'updated' ? 'would-update' : 'would-create',
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (hasManagedSection && !options?.overwrite) {
|
|
182
|
+
throw new Error(`${targetPath} already contains an Orizu CLI section. Pass --yes to replace it.`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
186
|
+
if (!exists) {
|
|
187
|
+
writeFileSync(targetPath, section, 'utf8')
|
|
188
|
+
return { target: 'agents-md', path: targetPath, action: 'created' }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (hasManagedSection) {
|
|
192
|
+
const pattern = new RegExp(
|
|
193
|
+
`${escapeRegExp(AGENTS_START_MARKER)}[\\s\\S]*?${escapeRegExp(AGENTS_END_MARKER)}\\n?`
|
|
194
|
+
)
|
|
195
|
+
writeFileSync(targetPath, existing.replace(pattern, section), 'utf8')
|
|
196
|
+
return { target: 'agents-md', path: targetPath, action: 'updated' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const existingPrefix = existing.trim().length === 0 ? '' : existing
|
|
200
|
+
const separator = existingPrefix.endsWith('\n') ? '\n' : '\n\n'
|
|
201
|
+
writeFileSync(targetPath, `${existingPrefix}${existingPrefix ? separator : ''}${section}`, 'utf8')
|
|
202
|
+
return { target: 'agents-md', path: targetPath, action: 'updated' }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function escapeRegExp(value: string): string {
|
|
206
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function targetNeedsOverwrite(
|
|
210
|
+
target: SkillInstallTarget,
|
|
211
|
+
options?: SkillInstallOptions
|
|
212
|
+
): boolean {
|
|
213
|
+
const targetPath = getSkillInstallPath(target, options)
|
|
214
|
+
if (!existsSync(targetPath)) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (target !== 'agents-md') {
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const existing = readFileSync(targetPath, 'utf8')
|
|
223
|
+
return existing.includes(AGENTS_START_MARKER) && existing.includes(AGENTS_END_MARKER)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function installSkillTarget(
|
|
227
|
+
target: SkillInstallTarget,
|
|
228
|
+
options?: SkillInstallOptions
|
|
229
|
+
): SkillInstallResult {
|
|
230
|
+
const sourceDir = resolveSkillSourceDir()
|
|
231
|
+
const targetPath = getSkillInstallPath(target, options)
|
|
232
|
+
const exists = existsSync(targetPath)
|
|
233
|
+
|
|
234
|
+
if (target === 'agents-md') {
|
|
235
|
+
return installAgentsMd(sourceDir, targetPath, options)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (options?.dryRun) {
|
|
239
|
+
return {
|
|
240
|
+
target,
|
|
241
|
+
path: targetPath,
|
|
242
|
+
action: exists ? 'would-update' : 'would-create',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (exists && !options?.overwrite) {
|
|
247
|
+
throw new Error(`${targetPath} already exists. Pass --yes to replace it.`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
copySkillTree(sourceDir, targetPath)
|
|
251
|
+
return {
|
|
252
|
+
target,
|
|
253
|
+
path: targetPath,
|
|
254
|
+
action: exists ? 'updated' : 'created',
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function isSkillInstallTarget(value: string): value is SkillInstallTarget {
|
|
259
|
+
return (SKILL_INSTALL_TARGETS as readonly string[]).includes(value)
|
|
260
|
+
}
|
|
@@ -68,8 +68,22 @@ def main() -> None:
|
|
|
68
68
|
parser.add_argument("--max-full-evals", type=positive_int)
|
|
69
69
|
parser.add_argument("--reflection-model", default=TextGepaConfig.reflection_model)
|
|
70
70
|
parser.add_argument("--reflection-temperature", type=float)
|
|
71
|
+
parser.add_argument("--reflection-max-tokens", type=positive_int)
|
|
71
72
|
parser.add_argument("--reflection-prompt-template")
|
|
72
73
|
parser.add_argument("--reflection-provider-settings")
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--skip-perfect-parent-reflection",
|
|
76
|
+
dest="skip_perfect_parent_reflection",
|
|
77
|
+
action="store_true",
|
|
78
|
+
default=TextGepaConfig.skip_perfect_parent_reflection,
|
|
79
|
+
help="Skip reflection and child creation when the parent scores perfectly on the minibatch (default).",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--no-skip-perfect-parent-reflection",
|
|
83
|
+
dest="skip_perfect_parent_reflection",
|
|
84
|
+
action="store_false",
|
|
85
|
+
help="Reflect and create a child even when the parent scores perfectly on the minibatch.",
|
|
86
|
+
)
|
|
73
87
|
parser.add_argument("--objective", default=TextGepaConfig.objective)
|
|
74
88
|
parser.add_argument("--seed", type=int, default=0)
|
|
75
89
|
parser.add_argument("--auto-promote", action="store_true")
|
|
@@ -94,8 +108,10 @@ def main() -> None:
|
|
|
94
108
|
max_full_evals=args.max_full_evals,
|
|
95
109
|
reflection_model=args.reflection_model,
|
|
96
110
|
reflection_temperature=args.reflection_temperature,
|
|
111
|
+
reflection_max_tokens=args.reflection_max_tokens,
|
|
97
112
|
reflection_prompt_template=read_text_arg(args.reflection_prompt_template),
|
|
98
113
|
reflection_provider_settings=read_json_object_arg(args.reflection_provider_settings, "--reflection-provider-settings") or {},
|
|
114
|
+
skip_perfect_parent_reflection=args.skip_perfect_parent_reflection,
|
|
99
115
|
objective=args.objective,
|
|
100
116
|
seed=args.seed,
|
|
101
117
|
auto_promote=args.auto_promote,
|
|
@@ -179,7 +179,7 @@ class TextGepaConfig:
|
|
|
179
179
|
max_candidate_proposals: int | None = None
|
|
180
180
|
reflection_model: str = "anthropic/claude-opus-4-7"
|
|
181
181
|
reflection_temperature: float | None = None
|
|
182
|
-
reflection_max_tokens: int =
|
|
182
|
+
reflection_max_tokens: int | None = None
|
|
183
183
|
reflection_prompt_template: str | None = None
|
|
184
184
|
reflection_provider_settings: dict[str, Any] = field(default_factory=dict)
|
|
185
185
|
objective: str = "Improve this text candidate to maximize evaluator score while preserving intended behavior."
|
|
@@ -189,6 +189,7 @@ class TextGepaConfig:
|
|
|
189
189
|
fail_on_log_error: bool = True
|
|
190
190
|
log_row_snapshots: bool = False
|
|
191
191
|
cache_evaluations: bool = True
|
|
192
|
+
skip_perfect_parent_reflection: bool = True
|
|
192
193
|
|
|
193
194
|
|
|
194
195
|
@dataclass(frozen=True)
|
|
@@ -319,6 +320,14 @@ def _is_better(challenger: float, incumbent: float, higher_is_better: bool) -> b
|
|
|
319
320
|
return challenger > incumbent if higher_is_better else challenger < incumbent
|
|
320
321
|
|
|
321
322
|
|
|
323
|
+
def _is_perfect_minibatch(results: list[RowEvaluation], higher_is_better: bool) -> bool:
|
|
324
|
+
if not results:
|
|
325
|
+
return False
|
|
326
|
+
if higher_is_better:
|
|
327
|
+
return all(result.score >= 1.0 for result in results)
|
|
328
|
+
return all(result.score <= 0.0 for result in results)
|
|
329
|
+
|
|
330
|
+
|
|
322
331
|
class EvaluationCache:
|
|
323
332
|
def __init__(self):
|
|
324
333
|
self._values: dict[str, RowEvaluation] = {}
|
|
@@ -420,37 +429,12 @@ def _aggregate_scores(
|
|
|
420
429
|
}
|
|
421
430
|
|
|
422
431
|
|
|
423
|
-
def
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
candidate_id
|
|
430
|
-
for candidate_ids in frontier_by_row_id.values()
|
|
431
|
-
for candidate_id in candidate_ids
|
|
432
|
-
}
|
|
433
|
-
for candidate_id in sorted(
|
|
434
|
-
remaining,
|
|
435
|
-
key=lambda value: aggregate_scores.get(value, 0.0),
|
|
436
|
-
reverse=not higher_is_better,
|
|
437
|
-
):
|
|
438
|
-
owned_rows = [
|
|
439
|
-
row_id
|
|
440
|
-
for row_id, candidate_ids in frontier_by_row_id.items()
|
|
441
|
-
if candidate_id in candidate_ids
|
|
442
|
-
]
|
|
443
|
-
if owned_rows and all(
|
|
444
|
-
any(other_id != candidate_id and other_id in remaining for other_id in frontier_by_row_id[row_id])
|
|
445
|
-
for row_id in owned_rows
|
|
446
|
-
):
|
|
447
|
-
remaining.discard(candidate_id)
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
row_id: {candidate_id for candidate_id in candidate_ids if candidate_id in remaining}
|
|
451
|
-
for row_id, candidate_ids in frontier_by_row_id.items()
|
|
452
|
-
if any(candidate_id in remaining for candidate_id in candidate_ids)
|
|
453
|
-
}
|
|
432
|
+
def _frontier_counts(frontier_by_row_id: dict[str, set[str]]) -> dict[str, int]:
|
|
433
|
+
counts: dict[str, int] = {}
|
|
434
|
+
for candidate_ids in frontier_by_row_id.values():
|
|
435
|
+
for candidate_id in candidate_ids:
|
|
436
|
+
counts[candidate_id] = counts.get(candidate_id, 0) + 1
|
|
437
|
+
return counts
|
|
454
438
|
|
|
455
439
|
|
|
456
440
|
def _select_parent_candidate_id(
|
|
@@ -479,15 +463,7 @@ def _select_parent_candidate_id(
|
|
|
479
463
|
"candidate_selection_strategy must be one of: pareto, current_best, epsilon_greedy"
|
|
480
464
|
)
|
|
481
465
|
|
|
482
|
-
|
|
483
|
-
_frontier_by_row_id(val_scores_by_candidate, higher_is_better),
|
|
484
|
-
_aggregate_scores(val_scores_by_candidate),
|
|
485
|
-
higher_is_better,
|
|
486
|
-
)
|
|
487
|
-
frontier_counts: dict[str, int] = {}
|
|
488
|
-
for candidate_ids in frontier.values():
|
|
489
|
-
for candidate_id in candidate_ids:
|
|
490
|
-
frontier_counts[candidate_id] = frontier_counts.get(candidate_id, 0) + 1
|
|
466
|
+
frontier_counts = _frontier_counts(_frontier_by_row_id(val_scores_by_candidate, higher_is_better))
|
|
491
467
|
sampling_list: list[str] = []
|
|
492
468
|
for candidate_id, count in sorted(frontier_counts.items()):
|
|
493
469
|
sampling_list.extend([candidate_id] * count)
|
|
@@ -505,15 +481,7 @@ def _parent_selection_payload(
|
|
|
505
481
|
higher_is_better: bool = True,
|
|
506
482
|
) -> dict[str, Any]:
|
|
507
483
|
aggregate_scores = _aggregate_scores(val_scores_by_candidate)
|
|
508
|
-
|
|
509
|
-
_frontier_by_row_id(val_scores_by_candidate, higher_is_better),
|
|
510
|
-
aggregate_scores,
|
|
511
|
-
higher_is_better,
|
|
512
|
-
)
|
|
513
|
-
frontier_counts: dict[str, int] = {}
|
|
514
|
-
for candidate_ids in frontier.values():
|
|
515
|
-
for candidate_id in candidate_ids:
|
|
516
|
-
frontier_counts[candidate_id] = frontier_counts.get(candidate_id, 0) + 1
|
|
484
|
+
frontier_counts = _frontier_counts(_frontier_by_row_id(val_scores_by_candidate, higher_is_better))
|
|
517
485
|
return {
|
|
518
486
|
"strategy": config.candidate_selection_strategy,
|
|
519
487
|
"selected_candidate_id": selected_candidate_id,
|
|
@@ -541,16 +509,23 @@ def _pareto_payload(
|
|
|
541
509
|
}
|
|
542
510
|
|
|
543
511
|
aggregate_scores = _aggregate_scores(val_scores_by_candidate)
|
|
544
|
-
|
|
512
|
+
frontier_counts = _frontier_counts(raw_frontier)
|
|
513
|
+
if frontier_counts:
|
|
514
|
+
best_candidate_id = max(
|
|
515
|
+
sorted(frontier_counts),
|
|
516
|
+
key=lambda candidate_id: (
|
|
517
|
+
frontier_counts[candidate_id],
|
|
518
|
+
aggregate_scores.get(candidate_id, 0.0) if higher_is_better else -aggregate_scores.get(candidate_id, 0.0),
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
best_candidate_id = max(aggregate_scores, key=aggregate_scores.get) if higher_is_better else min(aggregate_scores, key=aggregate_scores.get)
|
|
545
523
|
best_score = aggregate_scores[best_candidate_id]
|
|
546
524
|
return best_candidate_id, best_score, {
|
|
547
525
|
"frontier_by_row_id": frontier_by_row_id,
|
|
548
|
-
"frontier_candidate_ids": sorted(
|
|
549
|
-
candidate_id
|
|
550
|
-
for candidate_ids in raw_frontier.values()
|
|
551
|
-
for candidate_id in candidate_ids
|
|
552
|
-
}),
|
|
526
|
+
"frontier_candidate_ids": sorted(frontier_counts),
|
|
553
527
|
"candidate_scores": aggregate_scores,
|
|
528
|
+
"frontier_counts": frontier_counts,
|
|
554
529
|
"best_candidate_id": best_candidate_id,
|
|
555
530
|
"best_score_mean": best_score,
|
|
556
531
|
"best_candidate_preview": val_text_by_candidate[best_candidate_id][:1200],
|
|
@@ -853,6 +828,43 @@ def optimize_loaded_text_candidate(
|
|
|
853
828
|
candidate_id=parent_candidate_id,
|
|
854
829
|
)
|
|
855
830
|
|
|
831
|
+
parent_is_perfect = _is_perfect_minibatch(parent_results, higher_is_better)
|
|
832
|
+
if config.skip_perfect_parent_reflection and parent_is_perfect:
|
|
833
|
+
event_sink.log_event(
|
|
834
|
+
"reflection_skipped",
|
|
835
|
+
{
|
|
836
|
+
"reason": "parent_minibatch_perfect",
|
|
837
|
+
"parent_candidate_id": parent_candidate_id,
|
|
838
|
+
"parent_score_total": parent_total,
|
|
839
|
+
"parent_score_mean": _mean(parent_results),
|
|
840
|
+
"row_ids": [row.id for row in minibatch],
|
|
841
|
+
"higher_is_better": higher_is_better,
|
|
842
|
+
"decision_rule": "skip reflection when every parent minibatch row is already perfect",
|
|
843
|
+
},
|
|
844
|
+
event_layer="extension",
|
|
845
|
+
iteration=iteration,
|
|
846
|
+
candidate_id=parent_candidate_id,
|
|
847
|
+
)
|
|
848
|
+
event_sink.log_event("budget_updated", budget.to_payload(), event_layer="extension", iteration=iteration)
|
|
849
|
+
event_sink.log_event(
|
|
850
|
+
"iteration_completed",
|
|
851
|
+
{
|
|
852
|
+
"parent_candidate_id": parent_candidate_id,
|
|
853
|
+
"child_candidate_id": None,
|
|
854
|
+
"parent_train_score_total": parent_total,
|
|
855
|
+
"child_train_score_total": None,
|
|
856
|
+
"child_validation_score": None,
|
|
857
|
+
"best_candidate_id": best_candidate_id,
|
|
858
|
+
"best_validation_score": best_score,
|
|
859
|
+
"skipped_reflection": True,
|
|
860
|
+
"skip_reason": "parent_minibatch_perfect",
|
|
861
|
+
"budget": budget.to_payload(),
|
|
862
|
+
},
|
|
863
|
+
iteration=iteration,
|
|
864
|
+
candidate_id=best_candidate_id,
|
|
865
|
+
)
|
|
866
|
+
continue
|
|
867
|
+
|
|
856
868
|
event_sink.log_event(
|
|
857
869
|
"reflection_started",
|
|
858
870
|
{
|
|
@@ -62,12 +62,30 @@ def _add_temperature(payload: dict[str, Any], config: TextGepaConfig) -> dict[st
|
|
|
62
62
|
return {**payload, "temperature": config.reflection_temperature}
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def _validate_reflection_max_tokens(value: int | None) -> None:
|
|
66
|
+
if value is not None and value <= 0:
|
|
67
|
+
raise RuntimeError("reflection_max_tokens must be positive")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _require_anthropic_reflection_max_tokens(value: int | None) -> int:
|
|
71
|
+
_validate_reflection_max_tokens(value)
|
|
72
|
+
if value is None:
|
|
73
|
+
raise RuntimeError("reflection_max_tokens is required for Anthropic reflection models")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
65
77
|
def build_anthropic_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
|
|
66
|
-
|
|
78
|
+
max_tokens = _require_anthropic_reflection_max_tokens(config.reflection_max_tokens)
|
|
79
|
+
base_payload: dict[str, Any] = {
|
|
67
80
|
"model": model,
|
|
68
|
-
"max_tokens":
|
|
81
|
+
"max_tokens": max_tokens,
|
|
69
82
|
"messages": [{"role": "user", "content": prompt}],
|
|
70
|
-
}
|
|
83
|
+
}
|
|
84
|
+
payload = _merge_provider_settings(
|
|
85
|
+
base_payload,
|
|
86
|
+
config.reflection_provider_settings,
|
|
87
|
+
reserved_keys={"model", "max_tokens", "messages"},
|
|
88
|
+
)
|
|
71
89
|
payload = _add_temperature(payload, config)
|
|
72
90
|
if "temperature" in payload and "thinking" in payload:
|
|
73
91
|
raise RuntimeError("reflection_temperature cannot be combined with Anthropic thinking")
|
|
@@ -75,11 +93,18 @@ def build_anthropic_reflection_payload(model: str, prompt: str, config: TextGepa
|
|
|
75
93
|
|
|
76
94
|
|
|
77
95
|
def build_openai_reflection_payload(model: str, prompt: str, config: TextGepaConfig) -> dict[str, Any]:
|
|
78
|
-
|
|
96
|
+
_validate_reflection_max_tokens(config.reflection_max_tokens)
|
|
97
|
+
base_payload: dict[str, Any] = {
|
|
79
98
|
"model": model,
|
|
80
99
|
"input": [{"role": "user", "content": prompt}],
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
}
|
|
101
|
+
if config.reflection_max_tokens is not None:
|
|
102
|
+
base_payload["max_output_tokens"] = config.reflection_max_tokens
|
|
103
|
+
payload = _merge_provider_settings(
|
|
104
|
+
base_payload,
|
|
105
|
+
config.reflection_provider_settings,
|
|
106
|
+
reserved_keys={"model", "input", "max_output_tokens"},
|
|
107
|
+
)
|
|
83
108
|
return _add_temperature(payload, config)
|
|
84
109
|
|
|
85
110
|
|