orizu 0.5.21 → 0.5.23
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 +46 -15
- package/dist/cleanup.d.ts +6 -0
- package/dist/cleanup.d.ts.map +1 -0
- package/dist/cleanup.js +27 -0
- 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 +37 -15
- package/dist/hosted-commands.d.ts +13 -0
- package/dist/hosted-commands.d.ts.map +1 -0
- package/dist/hosted-commands.js +10 -0
- package/dist/hosted-gepa-cli.d.ts +11 -0
- package/dist/hosted-gepa-cli.d.ts.map +1 -0
- package/dist/hosted-gepa-cli.js +110 -0
- package/dist/hosted-gepa-numeric-values.d.ts +3 -0
- package/dist/hosted-gepa-numeric-values.d.ts.map +1 -0
- package/dist/hosted-gepa-numeric-values.js +49 -0
- package/dist/hosted-provider-settings.d.ts +3 -0
- package/dist/hosted-provider-settings.d.ts.map +1 -0
- package/dist/hosted-provider-settings.js +69 -0
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +6 -1
- package/dist/http.d.ts +1 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -74
- 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/optimizations-promote-cli.d.ts +3 -0
- package/dist/optimizations-promote-cli.d.ts.map +1 -1
- package/dist/optimizations-promote-cli.js +75 -7
- package/dist/runner-dir-verify.d.ts.map +1 -1
- package/dist/runner-dir-verify.js +4 -7
- package/dist/skilled-proposer-bake-report.d.ts +2 -0
- package/dist/skilled-proposer-bake-report.d.ts.map +1 -0
- package/dist/skilled-proposer-bake-report.js +1 -0
- 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 +26 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +103 -6
- package/package.json +1 -1
- package/scripts/vendor-gepa-python.mjs +81 -12
- package/src/cleanup.ts +37 -0
- package/src/gepa-engine-dispatch.ts +21 -1
- package/src/gepa-python-paths.ts +24 -6
- package/src/help.ts +37 -15
- package/src/hosted-commands.ts +4 -0
- package/src/hosted-gepa-cli.ts +107 -0
- package/src/hosted-gepa-numeric-values.ts +60 -0
- package/src/hosted-provider-settings.ts +69 -0
- package/src/hosted-session-cli.ts +4 -1
- package/src/http.ts +8 -0
- package/src/index.ts +112 -89
- package/src/instruction-set-loader.ts +55 -5
- package/src/instruction-sets-cli.ts +173 -19
- package/src/optimizations-promote-cli.ts +85 -7
- package/src/runner-dir-verify.ts +11 -6
- package/src/skilled-proposer-bake-report.ts +1 -0
- package/src/skilled-proposer-config.ts +191 -0
- package/src/skilled-proposer-launch.ts +96 -7
- package/src/skilled-proposer-venv-manager.mjs +44 -27
- 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/runtime.py +7 -2
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +15 -0
- package/vendor/skills/orizu-cli/SKILL.md +94 -168
- package/vendor/skills/orizu-cli/references/assess-and-plan.md +107 -0
- package/vendor/skills/orizu-cli/references/building-judges.md +442 -180
- package/vendor/skills/orizu-cli/references/cli-reference.md +69 -39
- package/vendor/skills/orizu-cli/references/dataset-design.md +100 -0
- package/vendor/skills/orizu-cli/references/eval-strategy.md +64 -0
- package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +115 -0
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +150 -82
- package/vendor/skills/orizu-cli/references/optimization-reports.md +196 -104
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +57 -37
- package/vendor/skills/orizu-cli/references/primer.md +43 -18
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +199 -131
|
@@ -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)
|
|
@@ -300,6 +300,7 @@ def main() -> None:
|
|
|
300
300
|
split_set_id=args.split_set_id,
|
|
301
301
|
train_split=args.train_split,
|
|
302
302
|
validation_split=args.val_split,
|
|
303
|
+
runner_version_id=prompt_context.runner_version_id,
|
|
303
304
|
metadata=run_metadata,
|
|
304
305
|
)
|
|
305
306
|
print(f"[orizu-gepa] started optimization run {run_id}", flush=True)
|
|
@@ -139,6 +139,7 @@ class OrizuClient:
|
|
|
139
139
|
train_split: str,
|
|
140
140
|
validation_split: str,
|
|
141
141
|
model_config_settings_version_id: str | None = None,
|
|
142
|
+
runner_version_id: str | None = None,
|
|
142
143
|
metadata: dict[str, Any] | None = None,
|
|
143
144
|
) -> str:
|
|
144
145
|
query = urllib.parse.urlencode({"project": project})
|
|
@@ -165,9 +166,23 @@ class OrizuClient:
|
|
|
165
166
|
"validationSplitName": validation_split,
|
|
166
167
|
"metadata": metadata or {},
|
|
167
168
|
**({"modelConfigSettingsVersionId": model_config_settings_version_id} if model_config_settings_version_id else {}),
|
|
169
|
+
**({"runnerVersionId": runner_version_id} if runner_version_id else {}),
|
|
168
170
|
})
|
|
169
171
|
return data["optimization_run_id"]
|
|
170
172
|
|
|
173
|
+
def require_hosted_optimization_run(self, *, run_id: str, project: str) -> str:
|
|
174
|
+
"""Resolve the existing project-scoped run before any runner executes."""
|
|
175
|
+
query = urllib.parse.urlencode({"project": project})
|
|
176
|
+
encoded_run_id = urllib.parse.quote(run_id, safe="")
|
|
177
|
+
data = self._request(
|
|
178
|
+
"GET",
|
|
179
|
+
f"/api/cli/optimization-runs/{encoded_run_id}?{query}",
|
|
180
|
+
)
|
|
181
|
+
run = data.get("optimizationRun") if isinstance(data, dict) else None
|
|
182
|
+
if not isinstance(run, dict) or run.get("id") != run_id:
|
|
183
|
+
raise RuntimeError("hosted_optimization_run_not_found")
|
|
184
|
+
return run_id
|
|
185
|
+
|
|
171
186
|
def fetch_exec_context(
|
|
172
187
|
self,
|
|
173
188
|
*,
|