orizu 0.5.21 → 0.5.22
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 +9 -6
- package/dist/gepa-engine-dispatch.d.ts.map +1 -1
- package/dist/gepa-engine-dispatch.js +20 -1
- package/dist/gepa-python-paths.d.ts.map +1 -1
- package/dist/gepa-python-paths.js +25 -6
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +36 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -39
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +57 -5
- package/dist/instruction-sets-cli.d.ts +5 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +134 -24
- package/dist/skilled-proposer-config.d.ts +2 -0
- package/dist/skilled-proposer-config.d.ts.map +1 -0
- package/dist/skilled-proposer-config.js +212 -0
- package/dist/skilled-proposer-launch.d.ts +4 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +65 -1
- package/package.json +1 -1
- package/src/gepa-engine-dispatch.ts +21 -1
- package/src/gepa-python-paths.ts +24 -6
- package/src/help.ts +36 -14
- package/src/index.ts +6 -53
- package/src/instruction-set-loader.ts +55 -5
- package/src/instruction-sets-cli.ts +173 -19
- package/src/skilled-proposer-config.ts +191 -0
- package/src/skilled-proposer-launch.ts +59 -1
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
- package/vendor/skills/orizu-cli/SKILL.md +50 -16
- package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
- package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
- package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
- package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
- package/vendor/skills/orizu-cli/references/primer.md +21 -5
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +132 -85
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import atexit
|
|
12
12
|
import dataclasses
|
|
13
|
+
import hashlib
|
|
13
14
|
import json
|
|
14
15
|
import os
|
|
15
16
|
import shutil
|
|
@@ -35,11 +36,16 @@ from orizu_gepa.optimizer import TextGepaConfig
|
|
|
35
36
|
|
|
36
37
|
_SELECTION_ENV = "ORIZU_CANDIDATE_PROPOSER"
|
|
37
38
|
_SELECTION_VALUE = "skilled-proposer"
|
|
39
|
+
_CONFIG_ENV = "ORIZU_SKILLED_PROPOSER_CONFIG"
|
|
38
40
|
_SUPPORTED_ROLES = {"system", "user", "assistant"}
|
|
39
41
|
_ALLOWED_DSPY_REQUEST_FIELDS = frozenset({"temperature", "max_tokens", "cache"})
|
|
40
42
|
_DspyBaseLM = dspy.BaseLM if dspy is not None else object
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
class _ProposalObservabilityEventFailure(RuntimeError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
43
49
|
def _require_dspy() -> Any:
|
|
44
50
|
if dspy is None:
|
|
45
51
|
raise RuntimeError("ALI_1505_DSPY_UNAVAILABLE")
|
|
@@ -90,6 +96,8 @@ class ProposalObservability:
|
|
|
90
96
|
self.durable_failure_root = durable_failure_root
|
|
91
97
|
self.events: list[dict[str, Any]] = []
|
|
92
98
|
self.budget = budget or ProposalCallBudget()
|
|
99
|
+
self._event_failure_count = 0
|
|
100
|
+
self._provider_failure_count = 0
|
|
93
101
|
self._temporary_root: Path | None = None
|
|
94
102
|
|
|
95
103
|
@classmethod
|
|
@@ -151,15 +159,50 @@ class ProposalObservability:
|
|
|
151
159
|
detail: str,
|
|
152
160
|
usage: dict[str, int] | None = None,
|
|
153
161
|
) -> None:
|
|
162
|
+
self._provider_failure_count += 1
|
|
154
163
|
self.budget.record(usage)
|
|
155
164
|
self._write_durable_failure(code=code, detail=detail, correlation_id=event.get("correlation_id"))
|
|
156
165
|
self._append_event(event)
|
|
157
166
|
|
|
158
167
|
def record_event_failure(self, error: Exception, *, correlation_id: str) -> None:
|
|
168
|
+
self._event_failure_count += 1
|
|
159
169
|
self._write_durable_failure(
|
|
160
170
|
code="proposal_observability_event_failed", detail=str(error), correlation_id=correlation_id,
|
|
161
171
|
)
|
|
162
172
|
|
|
173
|
+
def record_proposer_failure(self, error: Exception) -> None:
|
|
174
|
+
self._write_durable_failure(code="proposal_generation_failed", detail=str(error))
|
|
175
|
+
|
|
176
|
+
def record_component_fallback(self, *, component: str, error: Exception) -> None:
|
|
177
|
+
event = {
|
|
178
|
+
"type": "proposal_component_fallback",
|
|
179
|
+
"component": component,
|
|
180
|
+
"policy": "keep",
|
|
181
|
+
"errorType": type(error).__name__,
|
|
182
|
+
"errorMessage": str(error),
|
|
183
|
+
}
|
|
184
|
+
try:
|
|
185
|
+
self._append_event(event)
|
|
186
|
+
except OSError as event_error:
|
|
187
|
+
self._write_durable_failure(
|
|
188
|
+
code="proposal_observability_event_failed", detail=str(event_error),
|
|
189
|
+
)
|
|
190
|
+
raise _ProposalObservabilityEventFailure(
|
|
191
|
+
"proposal_observability_event_failed",
|
|
192
|
+
) from event_error
|
|
193
|
+
|
|
194
|
+
def write_proposer_identity(self, identity: dict[str, Any]) -> str:
|
|
195
|
+
root = self.event_log_root.parent
|
|
196
|
+
path = root / "proposer.json"
|
|
197
|
+
try:
|
|
198
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
path.write_text(json.dumps(identity, sort_keys=True) + "\n", encoding="utf-8")
|
|
200
|
+
except OSError as error:
|
|
201
|
+
code = "skilled_proposer_identity_write_failed"
|
|
202
|
+
self._write_durable_failure(code=code, detail=str(error))
|
|
203
|
+
raise RuntimeError(code) from error
|
|
204
|
+
return str(path)
|
|
205
|
+
|
|
163
206
|
def terminal_stats(self) -> dict[str, Any]:
|
|
164
207
|
return {"call_count": self.budget.call_count, "usage": dict(self.budget.usage)}
|
|
165
208
|
|
|
@@ -182,6 +225,14 @@ class ProposalObservability:
|
|
|
182
225
|
def has_safe_boundary_stop(self) -> bool:
|
|
183
226
|
return self.budget.has_safe_boundary_stop()
|
|
184
227
|
|
|
228
|
+
@property
|
|
229
|
+
def provider_failure_count(self) -> int:
|
|
230
|
+
return self._provider_failure_count
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def event_failure_count(self) -> int:
|
|
234
|
+
return self._event_failure_count
|
|
235
|
+
|
|
185
236
|
|
|
186
237
|
def _reject(message: str, *, code: str = "proposal_dspy_request_rejected") -> Any:
|
|
187
238
|
return _require_dspy().LMError(message, code=code)
|
|
@@ -311,9 +362,11 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
|
|
|
311
362
|
)
|
|
312
363
|
except Exception as event_error:
|
|
313
364
|
self._observability.record_event_failure(event_error, correlation_id=correlation_id)
|
|
314
|
-
raise
|
|
365
|
+
raise _ProposalObservabilityEventFailure(
|
|
366
|
+
"proposal_observability_event_failed",
|
|
367
|
+
) from event_error
|
|
315
368
|
raise dspy_module.LMError(
|
|
316
|
-
|
|
369
|
+
"proposal_provider_transport_failed",
|
|
317
370
|
code="proposal_provider_transport_failed",
|
|
318
371
|
model=self.model,
|
|
319
372
|
provider="orizu",
|
|
@@ -337,7 +390,9 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
|
|
|
337
390
|
self._observability.record_success(success_event, usage)
|
|
338
391
|
except Exception as error:
|
|
339
392
|
self._observability.record_event_failure(error, correlation_id=correlation_id)
|
|
340
|
-
raise
|
|
393
|
+
raise _ProposalObservabilityEventFailure(
|
|
394
|
+
"proposal_observability_event_failed",
|
|
395
|
+
) from error
|
|
341
396
|
return dspy_module.LMResponse.from_text(completion.text, model=self.model, usage=usage, cache_hit=False)
|
|
342
397
|
|
|
343
398
|
|
|
@@ -359,6 +414,268 @@ def make_skilled_proposer_bridge(
|
|
|
359
414
|
)
|
|
360
415
|
|
|
361
416
|
|
|
417
|
+
@dataclass(frozen=True)
|
|
418
|
+
class _NormalizedText:
|
|
419
|
+
source: str
|
|
420
|
+
content: str
|
|
421
|
+
sha256: str
|
|
422
|
+
byte_length: int
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@dataclass(frozen=True)
|
|
426
|
+
class _NormalizedSkill(_NormalizedText):
|
|
427
|
+
name: str
|
|
428
|
+
description: str | None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@dataclass(frozen=True)
|
|
432
|
+
class _SkilledProposerConfig:
|
|
433
|
+
config_file_sha256: str
|
|
434
|
+
implementation: str
|
|
435
|
+
package_version: str
|
|
436
|
+
config_sha256: str
|
|
437
|
+
skills: tuple[_NormalizedSkill, ...]
|
|
438
|
+
additional_instructions: _NormalizedText | None
|
|
439
|
+
base_instructions: _NormalizedText | None
|
|
440
|
+
max_words: int | None
|
|
441
|
+
max_tokens: int | None
|
|
442
|
+
max_examples: int | None
|
|
443
|
+
on_error: str
|
|
444
|
+
|
|
445
|
+
def identity(self) -> dict[str, Any]:
|
|
446
|
+
return {
|
|
447
|
+
"implementation": self.implementation,
|
|
448
|
+
"packageVersion": self.package_version,
|
|
449
|
+
"configSha256": self.config_sha256,
|
|
450
|
+
"skills": [{
|
|
451
|
+
"name": skill.name,
|
|
452
|
+
"source": skill.source,
|
|
453
|
+
"sha256": skill.sha256,
|
|
454
|
+
"byteLength": skill.byte_length,
|
|
455
|
+
} for skill in self.skills],
|
|
456
|
+
"additionalInstructionsSha256": (
|
|
457
|
+
self.additional_instructions.sha256 if self.additional_instructions else None
|
|
458
|
+
),
|
|
459
|
+
"baseInstructionsSha256": self.base_instructions.sha256 if self.base_instructions else None,
|
|
460
|
+
"maxWords": self.max_words,
|
|
461
|
+
"maxTokens": self.max_tokens,
|
|
462
|
+
"maxExamples": self.max_examples,
|
|
463
|
+
"onError": self.on_error,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _config_error(code: str, field_name: str) -> Any:
|
|
468
|
+
raise RuntimeError(f"{code}: {field_name}")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _strict_object(value: Any, fields: set[str], field_name: str) -> dict[str, Any]:
|
|
472
|
+
if not isinstance(value, dict):
|
|
473
|
+
return _config_error("skilled_proposer_config_invalid_field", field_name)
|
|
474
|
+
unknown = sorted(set(value) - fields)
|
|
475
|
+
if unknown:
|
|
476
|
+
return _config_error(
|
|
477
|
+
"skilled_proposer_config_unknown_field",
|
|
478
|
+
f"{field_name + '.' if field_name else ''}{unknown[0]}",
|
|
479
|
+
)
|
|
480
|
+
missing = sorted(fields - set(value))
|
|
481
|
+
if missing:
|
|
482
|
+
return _config_error(
|
|
483
|
+
"skilled_proposer_config_missing_field",
|
|
484
|
+
f"{field_name + '.' if field_name else ''}{missing[0]}",
|
|
485
|
+
)
|
|
486
|
+
return value
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _required_string(value: Any, field_name: str, *, allow_empty: bool = False) -> str:
|
|
490
|
+
if not isinstance(value, str) or (not allow_empty and not value.strip()):
|
|
491
|
+
return _config_error("skilled_proposer_config_invalid_field", field_name)
|
|
492
|
+
try:
|
|
493
|
+
value.encode("utf-8")
|
|
494
|
+
except UnicodeEncodeError:
|
|
495
|
+
return _config_error("skilled_proposer_config_invalid_unicode", field_name)
|
|
496
|
+
return value
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _positive_int_or_none(value: Any, field_name: str) -> int | None:
|
|
500
|
+
if value is None:
|
|
501
|
+
return None
|
|
502
|
+
if type(value) is not int or value <= 0:
|
|
503
|
+
return _config_error("skilled_proposer_config_invalid_field", field_name)
|
|
504
|
+
return value
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _normalized_text(value: Any, field_name: str) -> _NormalizedText:
|
|
508
|
+
raw = _strict_object(value, {"source", "content", "sha256", "byteLength"}, field_name)
|
|
509
|
+
source = _required_string(raw["source"], f"{field_name}.source")
|
|
510
|
+
content = _required_string(raw["content"], f"{field_name}.content")
|
|
511
|
+
digest = _required_string(raw["sha256"], f"{field_name}.sha256")
|
|
512
|
+
byte_length = raw["byteLength"]
|
|
513
|
+
if (len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest)
|
|
514
|
+
or type(byte_length) is not int or byte_length != len(content.encode("utf-8"))):
|
|
515
|
+
return _config_error("skilled_proposer_config_invalid_field", field_name)
|
|
516
|
+
if hashlib.sha256(content.encode()).hexdigest() != digest:
|
|
517
|
+
return _config_error("skilled_proposer_config_invalid_field", f"{field_name}.sha256")
|
|
518
|
+
return _NormalizedText(source, content, digest, byte_length)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _optional_normalized_text(value: Any, field_name: str) -> _NormalizedText | None:
|
|
522
|
+
return None if value is None else _normalized_text(value, field_name)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _normalized_text_payload(value: _NormalizedText | None) -> dict[str, Any] | None:
|
|
526
|
+
if value is None:
|
|
527
|
+
return None
|
|
528
|
+
return {
|
|
529
|
+
"source": value.source, "content": value.content,
|
|
530
|
+
"sha256": value.sha256, "byteLength": value.byte_length,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _parse_skilled_proposer_config(raw_payload: str) -> _SkilledProposerConfig:
|
|
535
|
+
try:
|
|
536
|
+
parsed = json.loads(raw_payload)
|
|
537
|
+
except (json.JSONDecodeError, TypeError) as error:
|
|
538
|
+
raise RuntimeError("skilled_proposer_config_invalid_json") from error
|
|
539
|
+
fields = {
|
|
540
|
+
"configFileSha256", "schemaVersion", "implementation", "packageVersion", "configSha256", "skills",
|
|
541
|
+
"additionalInstructions", "baseInstructions", "maxWords", "maxTokens", "maxExamples", "onError",
|
|
542
|
+
}
|
|
543
|
+
raw = _strict_object(parsed, fields, "")
|
|
544
|
+
if (type(raw["schemaVersion"]) is not int or raw["schemaVersion"] != 1
|
|
545
|
+
or raw["implementation"] != "cmpnd-ai/skilled-proposer"):
|
|
546
|
+
return _config_error("skilled_proposer_config_invalid_field", "schemaVersion or implementation")
|
|
547
|
+
package_version = _required_string(raw["packageVersion"], "packageVersion")
|
|
548
|
+
config_file_sha256 = _required_string(raw["configFileSha256"], "configFileSha256")
|
|
549
|
+
config_sha256 = _required_string(raw["configSha256"], "configSha256")
|
|
550
|
+
for field_name, digest in (("configFileSha256", config_file_sha256), ("configSha256", config_sha256)):
|
|
551
|
+
if len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest):
|
|
552
|
+
return _config_error("skilled_proposer_config_invalid_field", field_name)
|
|
553
|
+
if not isinstance(raw["skills"], list):
|
|
554
|
+
return _config_error("skilled_proposer_config_invalid_field", "skills")
|
|
555
|
+
skills: list[_NormalizedSkill] = []
|
|
556
|
+
for index, value in enumerate(raw["skills"]):
|
|
557
|
+
field_name = f"skills[{index}]"
|
|
558
|
+
skill_raw = _strict_object(
|
|
559
|
+
value, {"name", "description", "source", "content", "sha256", "byteLength"}, field_name,
|
|
560
|
+
)
|
|
561
|
+
text = _normalized_text({key: skill_raw[key] for key in (
|
|
562
|
+
"source", "content", "sha256", "byteLength",
|
|
563
|
+
)}, field_name)
|
|
564
|
+
name = _required_string(skill_raw["name"], f"{field_name}.name")
|
|
565
|
+
description = skill_raw["description"]
|
|
566
|
+
if description is not None:
|
|
567
|
+
description = _required_string(description, f"{field_name}.description", allow_empty=True)
|
|
568
|
+
skills.append(_NormalizedSkill(
|
|
569
|
+
source=text.source, content=text.content, sha256=text.sha256, byte_length=text.byte_length,
|
|
570
|
+
name=name, description=description,
|
|
571
|
+
))
|
|
572
|
+
if len({skill.name for skill in skills}) != len(skills):
|
|
573
|
+
return _config_error("skilled_proposer_config_invalid_field", "skills duplicate names")
|
|
574
|
+
on_error = raw["onError"]
|
|
575
|
+
if on_error not in ("keep", "raise"):
|
|
576
|
+
return _config_error("skilled_proposer_config_invalid_field", "onError")
|
|
577
|
+
result = _SkilledProposerConfig(
|
|
578
|
+
config_file_sha256=config_file_sha256,
|
|
579
|
+
implementation=raw["implementation"], package_version=package_version,
|
|
580
|
+
config_sha256=config_sha256, skills=tuple(skills),
|
|
581
|
+
additional_instructions=_optional_normalized_text(raw["additionalInstructions"], "additionalInstructions"),
|
|
582
|
+
base_instructions=_optional_normalized_text(raw["baseInstructions"], "baseInstructions"),
|
|
583
|
+
max_words=_positive_int_or_none(raw["maxWords"], "maxWords"),
|
|
584
|
+
max_tokens=_positive_int_or_none(raw["maxTokens"], "maxTokens"),
|
|
585
|
+
max_examples=_positive_int_or_none(raw["maxExamples"], "maxExamples"),
|
|
586
|
+
on_error=on_error,
|
|
587
|
+
)
|
|
588
|
+
effective = {
|
|
589
|
+
"configFileSha256": result.config_file_sha256,
|
|
590
|
+
"schemaVersion": 1,
|
|
591
|
+
"implementation": result.implementation,
|
|
592
|
+
"packageVersion": result.package_version,
|
|
593
|
+
"skills": [{
|
|
594
|
+
"name": skill.name, "description": skill.description, "source": skill.source,
|
|
595
|
+
"content": skill.content, "sha256": skill.sha256, "byteLength": skill.byte_length,
|
|
596
|
+
} for skill in result.skills],
|
|
597
|
+
"additionalInstructions": _normalized_text_payload(result.additional_instructions),
|
|
598
|
+
"baseInstructions": _normalized_text_payload(result.base_instructions),
|
|
599
|
+
"maxWords": result.max_words, "maxTokens": result.max_tokens,
|
|
600
|
+
"maxExamples": result.max_examples, "onError": result.on_error,
|
|
601
|
+
}
|
|
602
|
+
canonical = json.dumps(effective, ensure_ascii=False, separators=(",", ":"))
|
|
603
|
+
if hashlib.sha256(canonical.encode("utf-8")).hexdigest() != result.config_sha256:
|
|
604
|
+
return _config_error("skilled_proposer_config_hash_mismatch", "configSha256")
|
|
605
|
+
return result
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class _CompressionFailureObserver:
|
|
609
|
+
def __init__(self) -> None:
|
|
610
|
+
self.failure_count = 0
|
|
611
|
+
self.last_error: Exception | None = None
|
|
612
|
+
|
|
613
|
+
def on_module_start(self, **_unused: Any) -> None:
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
def on_module_end(self, *, exception: Exception | None = None, **_unused: Any) -> None:
|
|
617
|
+
if exception is not None:
|
|
618
|
+
self.failure_count += 1
|
|
619
|
+
self.last_error = exception
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class _ObservableConfiguredProposer:
|
|
623
|
+
def __init__(self, proposer: Any, observability: ProposalObservability, on_error: str) -> None:
|
|
624
|
+
self.proposer = proposer
|
|
625
|
+
self.observability = observability
|
|
626
|
+
self.on_error = on_error
|
|
627
|
+
self.compression_observer = _CompressionFailureObserver()
|
|
628
|
+
proposer.module.compress.callbacks.append(self.compression_observer)
|
|
629
|
+
|
|
630
|
+
def __getattr__(self, name: str) -> Any:
|
|
631
|
+
return getattr(self.proposer, name)
|
|
632
|
+
|
|
633
|
+
def __call__(self, candidate: dict[str, str], reflective_dataset: Any,
|
|
634
|
+
components_to_update: list[str]) -> dict[str, str]:
|
|
635
|
+
results: dict[str, str] = {}
|
|
636
|
+
for component in components_to_update:
|
|
637
|
+
compression_failures_before = self.compression_observer.failure_count
|
|
638
|
+
event_failures_before = self.observability.event_failure_count
|
|
639
|
+
failures_before = self.observability.provider_failure_count
|
|
640
|
+
try:
|
|
641
|
+
proposed = self.proposer(candidate, reflective_dataset, [component])
|
|
642
|
+
except Exception as error:
|
|
643
|
+
if self.observability.event_failure_count > event_failures_before:
|
|
644
|
+
raise RuntimeError("proposal_observability_event_failed") from error
|
|
645
|
+
if isinstance(error, _ProposalObservabilityEventFailure):
|
|
646
|
+
raise RuntimeError("proposal_observability_event_failed") from error
|
|
647
|
+
if isinstance(error, _require_dspy().LMError) and self.on_error == "keep":
|
|
648
|
+
raise
|
|
649
|
+
if self.on_error == "raise":
|
|
650
|
+
code = getattr(error, "code", None)
|
|
651
|
+
if code != "proposal_provider_transport_failed":
|
|
652
|
+
code = "proposal_generation_failed"
|
|
653
|
+
self.observability.record_proposer_failure(error)
|
|
654
|
+
raise RuntimeError(code or "proposal_observability_event_failed") from error
|
|
655
|
+
self.observability.record_component_fallback(component=component, error=error)
|
|
656
|
+
results[component] = candidate[component]
|
|
657
|
+
continue
|
|
658
|
+
if self.observability.event_failure_count > event_failures_before:
|
|
659
|
+
raise RuntimeError("proposal_observability_event_failed")
|
|
660
|
+
provider_failed = self.observability.provider_failure_count > failures_before
|
|
661
|
+
compression_failed = self.compression_observer.failure_count > compression_failures_before
|
|
662
|
+
if compression_failed or provider_failed:
|
|
663
|
+
compression_error = self.compression_observer.last_error if compression_failed else None
|
|
664
|
+
if isinstance(compression_error, _ProposalObservabilityEventFailure):
|
|
665
|
+
raise RuntimeError("proposal_observability_event_failed") from compression_error
|
|
666
|
+
error = (_require_dspy().LMError(
|
|
667
|
+
"proposal_compression_provider_failed: compression provider failure was suppressed upstream",
|
|
668
|
+
code="proposal_compression_provider_failed",
|
|
669
|
+
) if provider_failed else RuntimeError("proposal_compression_failed"))
|
|
670
|
+
if self.on_error == "raise":
|
|
671
|
+
raise error from compression_error
|
|
672
|
+
self.observability.record_component_fallback(component=component, error=error)
|
|
673
|
+
results[component] = candidate[component]
|
|
674
|
+
else:
|
|
675
|
+
results.update(proposed)
|
|
676
|
+
return results
|
|
677
|
+
|
|
678
|
+
|
|
362
679
|
def make_skilled_proposer_from_environment(
|
|
363
680
|
*,
|
|
364
681
|
config: TextGepaConfig,
|
|
@@ -369,7 +686,7 @@ def make_skilled_proposer_from_environment(
|
|
|
369
686
|
"""Create the opt-in proposer only for the exact selected environment value."""
|
|
370
687
|
if os.environ.get(_SELECTION_ENV) != _SELECTION_VALUE:
|
|
371
688
|
return None
|
|
372
|
-
from skilled_proposer import SkilledProposer
|
|
689
|
+
from skilled_proposer import Skill, SkilledProposer
|
|
373
690
|
|
|
374
691
|
bridge = make_skilled_proposer_bridge(
|
|
375
692
|
config=config,
|
|
@@ -378,4 +695,36 @@ def make_skilled_proposer_from_environment(
|
|
|
378
695
|
endpoint_override=endpoint_override,
|
|
379
696
|
ssl_cert_file=os.environ.get("SSL_CERT_FILE"),
|
|
380
697
|
)
|
|
381
|
-
|
|
698
|
+
raw_proposer_config = os.environ.get(_CONFIG_ENV)
|
|
699
|
+
if raw_proposer_config is None:
|
|
700
|
+
return SkilledProposer(prompt_model=bridge)
|
|
701
|
+
if raw_proposer_config.startswith("@"):
|
|
702
|
+
payload_path = Path(raw_proposer_config[1:])
|
|
703
|
+
try:
|
|
704
|
+
raw_proposer_config = payload_path.read_text(encoding="utf-8")
|
|
705
|
+
except (OSError, UnicodeError) as error:
|
|
706
|
+
raise RuntimeError("skilled_proposer_config_payload_unreadable") from error
|
|
707
|
+
try:
|
|
708
|
+
payload_path.unlink()
|
|
709
|
+
if payload_path.parent.name.startswith("orizu-skilled-proposer-payload-"):
|
|
710
|
+
payload_path.parent.rmdir()
|
|
711
|
+
except OSError:
|
|
712
|
+
pass
|
|
713
|
+
proposer_config = _parse_skilled_proposer_config(raw_proposer_config)
|
|
714
|
+
proposer = SkilledProposer(
|
|
715
|
+
skills=[Skill(
|
|
716
|
+
name=skill.name, description=skill.description, content=skill.content,
|
|
717
|
+
) for skill in proposer_config.skills],
|
|
718
|
+
additional_instructions=(
|
|
719
|
+
proposer_config.additional_instructions.content
|
|
720
|
+
if proposer_config.additional_instructions else None
|
|
721
|
+
),
|
|
722
|
+
base_instructions=(proposer_config.base_instructions.content if proposer_config.base_instructions else None),
|
|
723
|
+
max_words=proposer_config.max_words,
|
|
724
|
+
max_tokens=proposer_config.max_tokens,
|
|
725
|
+
max_examples=proposer_config.max_examples,
|
|
726
|
+
on_error="raise",
|
|
727
|
+
prompt_model=bridge,
|
|
728
|
+
)
|
|
729
|
+
observability.write_proposer_identity(proposer_config.identity())
|
|
730
|
+
return _ObservableConfiguredProposer(proposer, observability, proposer_config.on_error)
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orizu-cli
|
|
3
|
-
description: Use when the user wants to improve an LLM application
|
|
3
|
+
description: Use when the user mentions Orizu or wants to measurably improve an LLM application, model, or agent; collect human feedback on model outputs; convert feedback into or build evals; optimize instructions (including one-off changes they will validate with evals); work with Orizu datasets, tasks, apps, instruction sets, model-config profiles, components, judges, scorers, runners, score runs, optimization runs, GEPA, or hill-climbing; migrate a prompts-era setup; or build continually learning agents. Start instruction artifacts at `orizu instructions`. Orizu builds evals first, then improves applications against them; its CLI covers feedback collection, eval creation, versioned instructions and evaluator artifacts, local runner execution, score submission, and instruction optimization. Do NOT use for instruction advice when the user explicitly does not want to set up evals.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Orizu
|
|
7
7
|
|
|
8
|
-
Orizu improves LLM applications by building evals first, then optimizing against them. The end-to-end loop has four steps: **Upload → Annotate → Judge → Optimize**.
|
|
8
|
+
Orizu improves LLM applications by building evals first, then optimizing against them. The end-to-end loop has four steps: **Upload → Annotate → Judge → Optimize**. Start instruction work with `orizu instructions`: it is the single surface for instruction sets, model-config profiles, their components, and the set-wide default. The CLI also covers human-eval collection, judges, scorers, runners, local runner execution, score submission, packaged GEPA-style text optimization, and live optimization logging. Do not create or mutate standalone prompts; use read-only prompt compatibility only to find the instruction set that owns legacy material.
|
|
9
9
|
|
|
10
10
|
For end-to-end methodology and rationale, read `references/primer.md`. The reference docs below cover specific stages in depth.
|
|
11
11
|
|
|
12
12
|
## When to step aside
|
|
13
13
|
|
|
14
14
|
Skip this skill when:
|
|
15
|
-
- The user wants a one-off
|
|
15
|
+
- The user wants a one-off instruction edit they explicitly won't validate with evals.
|
|
16
16
|
- The user has already said they don't want to set up evals.
|
|
17
|
-
- The request is generic
|
|
17
|
+
- The request is generic instruction-writing theory unrelated to a specific app.
|
|
18
18
|
|
|
19
19
|
# Orizu CLI basics
|
|
20
20
|
|
|
@@ -54,6 +54,24 @@ Projects live inside teams. Resolve or create both before any workflow command.
|
|
|
54
54
|
|
|
55
55
|
For team membership, app, dataset, and task command surfaces, see `references/cli-reference.md`.
|
|
56
56
|
|
|
57
|
+
## Instruction artifacts: start here
|
|
58
|
+
|
|
59
|
+
An instruction set is one customer-facing identity for an agent's instructions. Its manifest carries a `name`, optional `description`, fixed, ordered `shape`, and component values. Each model config has its own profile, and each profile owns its own component values; components are never shared between profiles or sets. The set's stable slug is its CLI reference even if the display name changes. Its default serves model configs that do not have a production profile version.
|
|
60
|
+
|
|
61
|
+
Use one command surface throughout:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
orizu instructions list --project <team>/<project>
|
|
65
|
+
orizu instructions show <slug-or-exact-name> --project <team>/<project>
|
|
66
|
+
# Human/local CLI only: a human runs create/push; hosted agents may run profiles new and sync.
|
|
67
|
+
orizu instructions create ./orizu.instruction-set.json --project <team>/<project> --model-config <identity>
|
|
68
|
+
orizu instructions push ./orizu.instruction-set.json --project <team>/<project> --set <slug-or-exact-name>
|
|
69
|
+
orizu instructions profiles new <slug-or-exact-name> --project <team>/<project> --model-config <identity>
|
|
70
|
+
orizu instructions sync <slug-or-exact-name> --out ./instructions --project <team>/<project>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For a prompts-era setup, read `references/instructions-after-prompts.md` before making changes. Legacy prompt read commands identify the owning instruction set and component key; continue from that set through `orizu instructions`.
|
|
74
|
+
|
|
57
75
|
# Evals best practices
|
|
58
76
|
|
|
59
77
|
Walk through the four steps in order — each depends on the output of the previous. Methodology and rationale: `references/primer.md`.
|
|
@@ -106,11 +124,11 @@ Deeper: `references/primer.md` (Step 2); `references/cli-reference.md` (apps + t
|
|
|
106
124
|
|
|
107
125
|
## 3. Judge — turn labels into automated evaluators
|
|
108
126
|
|
|
109
|
-
Use
|
|
127
|
+
Use `orizu instructions` for the application instruction material being evaluated. Judges remain versioned evaluator artifacts with runner zips, scorer definitions, and submitted score results; read **`references/prompt-control-plane.md`** before using those compatible evaluator commands. When the user mentions legacy prompt feedback or comments, inspect the read-only threads with `orizu comments list --prompt <prompt-id-or-name> --project <team>/<project>`, find the owning instruction set, and revise that set through its manifest.
|
|
110
128
|
|
|
111
129
|
Judge construction details are still useful when authoring the evaluator itself.
|
|
112
130
|
|
|
113
|
-
A judge is the evaluator
|
|
131
|
+
A judge is the evaluator artifact. A scorer is the metric contract that names what score is being produced and how it should be displayed, compared, and used in optimizations. For UI-visible instruction performance, register a scorer and submit score runs; do not rely only on raw `runs submit`.
|
|
114
132
|
|
|
115
133
|
Set scorers are aggregate metric contracts. Use `orizu scorers exec` for builtin set metrics such as Cohen's kappa, accuracy, precision, recall, and F1; it consumes subject results or dependency row-scorer results, computes one aggregate, stores row evidence, and submits a canonical score run by default. If you already computed an aggregate locally, use `orizu scores submit --aggregate` with explicit `scoreValue`, `diagnostics`, `feedbackSummary`, and optional `rowEvidence`. `runners exec --scorer-version` remains a low-level row-runner compatibility command. Use a row-mode scorer, not a set scorer, for GEPA reflection because reflection needs per-row feedback.
|
|
116
134
|
|
|
@@ -131,7 +149,7 @@ Detailed walkthrough — code assertion patterns, LLM-judge prompt scaffold, tra
|
|
|
131
149
|
|
|
132
150
|
## 4. Optimize — hill-climb against validated judges
|
|
133
151
|
|
|
134
|
-
Use the
|
|
152
|
+
Use the instruction control plane when the user wants an optimizer run to appear in Orizu, stream live events, or promote accepted candidates into the target instruction-set profile. For text-candidate GEPA-style optimization, prefer the bundled `orizu optimizations run-gepa` flow. Read **`references/prompt-control-plane.md`** before wiring those endpoints.
|
|
135
153
|
|
|
136
154
|
GEPA details below are still useful for local optimizer implementation. DSPy is only relevant when the customer already uses DSPy or asks for an external DSPy GEPA implementation.
|
|
137
155
|
|
|
@@ -153,9 +171,9 @@ Local execution workflow:
|
|
|
153
171
|
Bundled `run-gepa` reflection behavior:
|
|
154
172
|
- **Scorer contract:** `run-gepa` sends the scorer runner a GEPA-shaped row (`{source_row, candidate_id, candidate_output, …}`), NOT the flat-row shape used by `runners exec --scorer-version`. A judge runner built for flat-row score runs will silently score every candidate 0 unless you pass `--scorer-input-contract flat_row` (add `--scorer-candidate-field <row-field>`, e.g. `draft`, when the judge reads the candidate output from a named row field). Do not hand-write an adapter runner — the flag applies the official adapter without changing the registered runner bytes, so it composes with `--scorer-runner-dir` content-sha verification. `run-gepa` validates the contract on the seed at launch and refuses a uniformly-worst-scoring seed with a diagnosis; `--allow-degenerate-seed` overrides only when the seed genuinely deserves the worst score everywhere. Both contracts: `references/prompt-control-plane.md` ("Scorer-Runner Input Contracts").
|
|
155
173
|
- Budget controls are mutually exclusive. Use at most one of `--budget auto|light|medium|heavy`, `--max-metric-calls <n>`, `--max-full-evals <n>`, `--max-iterations <n>`, or `--max-candidate-proposals <n>`. The proposal cap is official-engine-only. If none is provided, `run-gepa` uses `--budget auto`, the balanced medium preset.
|
|
156
|
-
- **Skilled proposer:** opt in with `--candidate-proposer skilled-proposer` on the official engine; the selected run prepares or reuses its managed Python environment automatically. Bound proposal work with `--proposal-max-tokens` and `--proposal-max-calls`, independently of `--max-metric-calls`. Read `references/prompt-control-plane.md` ("Skilled proposer") for validation and recovery; readiness is proven when optimizer execution begins without an `ALI_1505_*` environment diagnostic.
|
|
157
|
-
- The reflective LM's final text
|
|
158
|
-
- Keep provider-native reasoning controls separate from the
|
|
174
|
+
- **Skilled proposer:** opt in with `--candidate-proposer skilled-proposer` on the official engine; the selected run prepares or reuses its managed Python environment automatically. Optionally add `--candidate-proposer-config @path` for explicit skills and guidance. The selection is incompatible with `--reflection-prompt-template`, even without a config. Bound aggregate proposal work with `--proposal-max-tokens` and `--proposal-max-calls`, independently of `--max-metric-calls`. Do not confuse that aggregate token budget with per-response `--reflection-max-tokens` or the config's proposed-instruction `maxTokens`. Read `references/prompt-control-plane.md` ("Skilled proposer") for the schema, validation, and managed-environment recovery; readiness is proven when optimizer execution begins without an `ALI_1505_*` environment diagnostic.
|
|
175
|
+
- The reflective LM's final text becomes the selected component's next value. It should return only the complete updated component value, not analysis, labels, XML tags, or markdown fences.
|
|
176
|
+
- Keep provider-native reasoning controls separate from the component text with `--reflection-provider-settings <json|@file>`.
|
|
159
177
|
- 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`.
|
|
160
178
|
- 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.
|
|
161
179
|
- 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.
|
|
@@ -172,7 +190,8 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
|
|
|
172
190
|
|
|
173
191
|
- `references/primer.md` — methodology end-to-end (read first when in doubt about *why*).
|
|
174
192
|
- `references/cli-reference.md` — full CLI command surface.
|
|
175
|
-
- `references/prompt-control-plane.md` —
|
|
193
|
+
- `references/prompt-control-plane.md` — instruction sets, judges, scorers, runners, score submission, optimizer event logging, bundled GEPA, and promotion endpoints.
|
|
194
|
+
- `references/instructions-after-prompts.md` — finding the instruction set that owns prompts-era material and consolidating legacy one-component sets.
|
|
176
195
|
- `references/building-apps.md` — labeler app contract, design principles, common patterns, offline smoke test.
|
|
177
196
|
- `references/building-judges.md` — judge/scorer authoring + TPR/TNR validation.
|
|
178
197
|
- `references/optimization-with-gepa.md` — GEPA optimization loop, with DSPy only as external context.
|
|
@@ -193,7 +212,7 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
|
|
|
193
212
|
- `--output-schema` JSON Schema validation surface is restricted to `type`, `required`, `properties`, `items`, `enum`.
|
|
194
213
|
- Export defaults: `--format jsonl`, output `<taskId>.<format>`.
|
|
195
214
|
- Task reports can be uploaded only after the task is `paused` or `completed`; rerun `tasks report set` or `tasks report upload` to replace the markdown report.
|
|
196
|
-
-
|
|
215
|
+
- Instruction and evaluation control-plane commands should use ids for dataset versions, split sets, component versions where compatibility commands require them, scorer versions, runner versions, optimizer versions, and optimization runs.
|
|
197
216
|
- Optimization exports default to `<run-id>.optimization.json`; prefer the existing `logs/<run-id>` directory from `run-gepa` when it is available because it contains the full local trace without needing server rehydration.
|
|
198
217
|
- When ending an optimization run, attach a markdown report with `--report-file` when possible. Use `references/optimization-reports.md` for what to include.
|
|
199
218
|
- Use `orizu comments list --prompt <prompt-id-or-name> --project <team>/<project> [--label <label> | --version <id>] [--json]` to list prompt-level discussion threads with open/resolved status, selected text/line context, and replies.
|
|
@@ -201,8 +220,23 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
|
|
|
201
220
|
|
|
202
221
|
# Where things live
|
|
203
222
|
|
|
204
|
-
- **Orizu is the source of truth for run metadata and pointers**: production
|
|
223
|
+
- **Orizu is the source of truth for run metadata and pointers**: instruction-set defaults, profile production pointers, scorer/runner pointers, version lineage, score runs, optimization runs, and report history all live in Orizu — query them with `orizu ...` commands (for example, `orizu instructions list`, `orizu instructions show`, and `orizu runs ...`), never by reading repo files.
|
|
205
224
|
- **The repo is the working set (files only)**: source, configs, `App.tsx`, schemas, and local traces under `logs/<run-id>`. It is the scratch space you edit and commit; it does NOT hold the canonical pointer/run state — that is in Orizu.
|
|
206
|
-
- **Hosted sessions ship a pre-authenticated CLI**: inside a hosted agent sandbox the `orizu` CLI is already authenticated via `ORIZU_TOKEN_FILE` (with `ORIZU_BASE_URL` pointing at the API). You do NOT run `orizu login` — just call `orizu ...`. The bearer carries **
|
|
207
|
-
- **
|
|
208
|
-
- **Human-only actions will 403**:
|
|
225
|
+
- **Hosted sessions ship a pre-authenticated CLI**: inside a hosted agent sandbox the `orizu` CLI is already authenticated via `ORIZU_TOKEN_FILE` (with `ORIZU_BASE_URL` pointing at the API). You do NOT run `orizu login` — just call `orizu ...`. The bearer carries **team-curator authority while your session is active**, except for the explicit human-only carveouts below (ADR-008 and the ALI-1558 instruction-write restriction). It can read and sync instruction sets and can perform ordinary curator work across the team. (The full per-role access matrix lives in the Orizu repo at `docs/authorization-matrix.md`.)
|
|
226
|
+
- **Hosted `--session` applies to git-canonical artifacts, not instruction sets**: instruction sets and profiles are control-plane canonical (ADR-023), and their CLI mutations do not accept `--session`. A hosted agent may inspect them, sync them, and prepare a complete manifest, but a human curator using the local CLI with a user token must run the six catalog mutations listed below and all pointer moves. Judges, scorers, runners, and optimizers remain git-canonical for teams with a workbench repository; their hosted writes must carry `--session <session-id>`, and their bytes must live as commits on the session branch. The remaining session-scoped legacy prompt compatibility is not a standalone instruction workflow. Before calling any artifact committed, run `git ls-files <path>` and inspect the commit; an Orizu version id alone does not prove the bytes exist in a branch.
|
|
227
|
+
- **Human-only actions will 403**: a hosted agent must hand the following commands
|
|
228
|
+
to a human curator using the local CLI with a user token. Human/local CLI only:
|
|
229
|
+
`orizu instructions create`, `orizu instructions push`,
|
|
230
|
+
`orizu instructions shape add`, `orizu instructions shape remove`,
|
|
231
|
+
`orizu instructions archive`, and `orizu instructions restore`.
|
|
232
|
+
Human/local CLI only: The measured response for `orizu instructions create` and `orizu instructions push` is `Sessionless registration is not available to the hosted agent: this artifact class is git-canonical (ADR-007) and its bytes must be committed to the session branch. Re-run with --session <session-id>.` Those commands have no `--session` flag, so do not retry them.
|
|
233
|
+
Human/local CLI only: The measured response for `orizu instructions shape add` and `orizu instructions shape remove` is `Agents cannot change instruction-set shapes`.
|
|
234
|
+
Human/local CLI only: The measured response for `orizu instructions archive` and `orizu instructions restore` is `Agent sessions cannot perform this action`.
|
|
235
|
+
Promotion-manifest **approval / apply / reject**, production and default
|
|
236
|
+
pointer flips, session/credential lifecycle (starting or ending hosted
|
|
237
|
+
sessions, minting tokens, deploy keys), integration/connector credential
|
|
238
|
+
reads (secrets reach your sandbox only as environment variables resolved at
|
|
239
|
+
boot — never via API/DB), and team/member/installation/billing management
|
|
240
|
+
are also human-only. Do not attempt them from a hosted session. Prepare the
|
|
241
|
+
manifest or proposal and hand it to a human curator to run, approve, or
|
|
242
|
+
apply.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Building Judges And Scorers
|
|
2
2
|
|
|
3
|
-
How to turn human-labeled annotations into automated evaluators. Use this reference for authoring and validating the evaluator logic; use `prompt-control-plane.md` to push versioned
|
|
3
|
+
How to turn human-labeled annotations into automated evaluators. Use this reference for authoring and validating the evaluator logic; use `prompt-control-plane.md` to push versioned judge artifacts, runner artifacts, scorer definitions, and score runs into Orizu.
|
|
4
4
|
|
|
5
5
|
## Inputs
|
|
6
6
|
|
|
@@ -138,7 +138,7 @@ Run the validated judge over future outputs to score them. Wire it into:
|
|
|
138
138
|
- CI/CD on a curated test set (block merges that drop pass rate)
|
|
139
139
|
- Online monitoring on sampled traffic (alert on regressions)
|
|
140
140
|
- Optimization loop as a metric (`optimization-with-gepa.md`)
|
|
141
|
-
- Orizu scorers and score runs (`prompt-control-plane.md`) so
|
|
141
|
+
- Orizu scorers and score runs (`prompt-control-plane.md`) so instruction-set detail, changelog, scorer detail, and optimization surfaces show comparable performance.
|
|
142
142
|
|
|
143
143
|
## Set scorers for judge-vs-gold agreement
|
|
144
144
|
|