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
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join, resolve } from 'path'
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_DIR_NAME = '.orizu'
|
|
5
|
+
|
|
6
|
+
export interface WorkspaceInitOptions {
|
|
7
|
+
cwd?: string
|
|
8
|
+
baseUrl?: string | null
|
|
9
|
+
cliVersion?: string | null
|
|
10
|
+
dryRun?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkspaceInitResult {
|
|
14
|
+
root: string
|
|
15
|
+
state: 'created' | 'exists' | 'would-create'
|
|
16
|
+
gitignoreUpdated: boolean
|
|
17
|
+
actions: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const WORKSPACE_README = `# Orizu workspace
|
|
21
|
+
|
|
22
|
+
Local Orizu artifacts for this repository. This folder is gitignored by
|
|
23
|
+
default: generated and server-derived content here is not guaranteed to stay
|
|
24
|
+
in sync with the Orizu server, so checking it in would risk drift. Safe,
|
|
25
|
+
user-authored configuration may move to a committed location once a sync model
|
|
26
|
+
exists.
|
|
27
|
+
|
|
28
|
+
- \`workspace.json\`: safe local metadata (no secrets; credentials stay in
|
|
29
|
+
\`~/.config/orizu\`).
|
|
30
|
+
- \`generated/\`: runner artifacts, exports, and other generated files.
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
export function getWorkspaceRoot(cwd?: string): string {
|
|
34
|
+
return resolve(cwd || process.cwd(), WORKSPACE_DIR_NAME)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function workspaceExists(cwd?: string): boolean {
|
|
38
|
+
return existsSync(join(getWorkspaceRoot(cwd), 'workspace.json'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureGitignoreEntry(cwd: string, dryRun: boolean): { updated: boolean, action: string | null } {
|
|
42
|
+
const gitignorePath = resolve(cwd, '.gitignore')
|
|
43
|
+
const entry = `${WORKSPACE_DIR_NAME}/`
|
|
44
|
+
|
|
45
|
+
if (existsSync(gitignorePath)) {
|
|
46
|
+
const existing = readFileSync(gitignorePath, 'utf8')
|
|
47
|
+
const hasEntry = existing
|
|
48
|
+
.split('\n')
|
|
49
|
+
.some(line => line.trim() === entry || line.trim() === WORKSPACE_DIR_NAME)
|
|
50
|
+
if (hasEntry) {
|
|
51
|
+
return { updated: false, action: null }
|
|
52
|
+
}
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
const separator = existing.endsWith('\n') || existing.length === 0 ? '' : '\n'
|
|
55
|
+
writeFileSync(gitignorePath, `${existing}${separator}\n# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8')
|
|
56
|
+
}
|
|
57
|
+
return { updated: true, action: `append ${entry} to .gitignore` }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!dryRun) {
|
|
61
|
+
writeFileSync(gitignorePath, `# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8')
|
|
62
|
+
}
|
|
63
|
+
return { updated: true, action: `create .gitignore with ${entry}` }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function initOrizuWorkspace(options?: WorkspaceInitOptions): WorkspaceInitResult {
|
|
67
|
+
const cwd = resolve(options?.cwd || process.cwd())
|
|
68
|
+
const root = getWorkspaceRoot(cwd)
|
|
69
|
+
const dryRun = Boolean(options?.dryRun)
|
|
70
|
+
const exists = workspaceExists(cwd)
|
|
71
|
+
const actions: string[] = []
|
|
72
|
+
|
|
73
|
+
if (!exists) {
|
|
74
|
+
actions.push(`create ${WORKSPACE_DIR_NAME}/workspace.json, README.md, and generated/`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gitignore = ensureGitignoreEntry(cwd, dryRun || exists)
|
|
78
|
+
if (!exists && gitignore.action) {
|
|
79
|
+
actions.push(gitignore.action)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
return {
|
|
84
|
+
root,
|
|
85
|
+
state: exists ? 'exists' : 'would-create',
|
|
86
|
+
gitignoreUpdated: false,
|
|
87
|
+
actions,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (exists) {
|
|
92
|
+
return { root, state: 'exists', gitignoreUpdated: false, actions: [] }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
mkdirSync(join(root, 'generated'), { recursive: true })
|
|
96
|
+
writeFileSync(join(root, 'README.md'), WORKSPACE_README, 'utf8')
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(root, 'workspace.json'),
|
|
99
|
+
`${JSON.stringify(
|
|
100
|
+
{
|
|
101
|
+
version: 1,
|
|
102
|
+
server: options?.baseUrl || null,
|
|
103
|
+
createdByCliVersion: options?.cliVersion || null,
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2
|
|
107
|
+
)}\n`,
|
|
108
|
+
'utf8'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return { root, state: 'created', gitignoreUpdated: gitignore.updated, actions }
|
|
112
|
+
}
|
|
@@ -3,6 +3,7 @@ from .optimizer import (
|
|
|
3
3
|
DatasetRow,
|
|
4
4
|
NumThreadsPlan,
|
|
5
5
|
PromptContext,
|
|
6
|
+
RetryableReflectionError,
|
|
6
7
|
RowEvaluation,
|
|
7
8
|
TextGepaConfig,
|
|
8
9
|
TextGepaResult,
|
|
@@ -15,6 +16,7 @@ __all__ = [
|
|
|
15
16
|
"DatasetRow",
|
|
16
17
|
"NumThreadsPlan",
|
|
17
18
|
"PromptContext",
|
|
19
|
+
"RetryableReflectionError",
|
|
18
20
|
"RowEvaluation",
|
|
19
21
|
"TextGepaConfig",
|
|
20
22
|
"TextGepaResult",
|
|
@@ -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)
|
|
@@ -46,6 +49,38 @@ def read_json_object_arg(value: str | None, flag_name: str) -> dict[str, Any] |
|
|
|
46
49
|
return parsed
|
|
47
50
|
|
|
48
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
|
+
|
|
49
84
|
def main() -> None:
|
|
50
85
|
parser = argparse.ArgumentParser(prog="orizu-gepa")
|
|
51
86
|
parser.add_argument("--project", required=True)
|
|
@@ -60,8 +95,8 @@ def main() -> None:
|
|
|
60
95
|
parser.add_argument("--split-set-id", required=True)
|
|
61
96
|
parser.add_argument("--train-split", default="train")
|
|
62
97
|
parser.add_argument("--val-split", default="validation")
|
|
63
|
-
parser.add_argument("--budget",
|
|
64
|
-
parser.add_argument("--max-iterations", type=positive_int
|
|
98
|
+
parser.add_argument("--budget", choices=["auto", "light", "medium", "heavy"])
|
|
99
|
+
parser.add_argument("--max-iterations", type=positive_int)
|
|
65
100
|
parser.add_argument("--minibatch-size", type=positive_int, default=3)
|
|
66
101
|
parser.add_argument("--num-threads", type=num_threads_arg, default=TextGepaConfig.num_threads)
|
|
67
102
|
parser.add_argument(
|
|
@@ -76,6 +111,18 @@ def main() -> None:
|
|
|
76
111
|
parser.add_argument("--reflection-model", default=TextGepaConfig.reflection_model)
|
|
77
112
|
parser.add_argument("--reflection-temperature", type=float)
|
|
78
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
|
+
)
|
|
79
126
|
parser.add_argument("--reflection-prompt-template")
|
|
80
127
|
parser.add_argument("--reflection-provider-settings")
|
|
81
128
|
parser.add_argument(
|
|
@@ -99,7 +146,12 @@ def main() -> None:
|
|
|
99
146
|
parser.add_argument("--log-dir", default="logs")
|
|
100
147
|
parser.add_argument("--no-local-log", action="store_true")
|
|
101
148
|
parser.add_argument("--metadata", default="{}")
|
|
102
|
-
|
|
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))
|
|
103
155
|
|
|
104
156
|
metadata = json.loads(args.metadata)
|
|
105
157
|
if not isinstance(metadata, dict):
|
|
@@ -117,6 +169,8 @@ def main() -> None:
|
|
|
117
169
|
reflection_model=args.reflection_model,
|
|
118
170
|
reflection_temperature=args.reflection_temperature,
|
|
119
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,
|
|
120
174
|
reflection_prompt_template=read_text_arg(args.reflection_prompt_template),
|
|
121
175
|
reflection_provider_settings=read_json_object_arg(args.reflection_provider_settings, "--reflection-provider-settings") or {},
|
|
122
176
|
skip_perfect_parent_reflection=args.skip_perfect_parent_reflection,
|
|
@@ -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(
|
|
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
|