orizu 0.5.19 → 0.5.21
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 +23 -0
- package/dist/archive-artifacts-cli.d.ts +50 -0
- package/dist/archive-artifacts-cli.d.ts.map +1 -0
- package/dist/artifact-archive.d.ts +48 -0
- package/dist/artifact-archive.d.ts.map +1 -0
- package/dist/artifact-pull.d.ts +22 -0
- package/dist/artifact-pull.d.ts.map +1 -0
- package/dist/banner.d.ts +3 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/connectors-cli.d.ts +31 -0
- package/dist/connectors-cli.d.ts.map +1 -0
- package/dist/credentials.d.ts +46 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/csv.d.ts +49 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/dataset-download.d.ts +6 -0
- package/dist/dataset-download.d.ts.map +1 -0
- package/dist/dataset-edit-rows.d.ts +10 -0
- package/dist/dataset-edit-rows.d.ts.map +1 -0
- package/dist/dataset-upload-snapshot.d.ts +13 -0
- package/dist/dataset-upload-snapshot.d.ts.map +1 -0
- package/dist/daytona-slice-rehearsal.d.ts +89 -0
- package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
- package/dist/daytona-slice.d.ts +106 -0
- package/dist/daytona-slice.d.ts.map +1 -0
- package/dist/deploy-key-merge-core.d.ts +121 -0
- package/dist/deploy-key-merge-core.d.ts.map +1 -0
- package/dist/diff-comments-cli.d.ts +12 -0
- package/dist/diff-comments-cli.d.ts.map +1 -0
- package/dist/diff-comments-cli.js +21 -2
- package/dist/egress-allowlist-cli.d.ts +23 -0
- package/dist/egress-allowlist-cli.d.ts.map +1 -0
- package/dist/egress-policy.d.ts +108 -0
- package/dist/egress-policy.d.ts.map +1 -0
- package/dist/error-response.d.ts +9 -0
- package/dist/error-response.d.ts.map +1 -0
- package/dist/file-parser.d.ts +5 -0
- package/dist/file-parser.d.ts.map +1 -0
- package/dist/gepa-engine-dispatch.d.ts +9 -0
- package/dist/gepa-engine-dispatch.d.ts.map +1 -0
- package/dist/gepa-engine-dispatch.js +307 -0
- package/dist/gepa-python-command.d.ts +6 -0
- package/dist/gepa-python-command.d.ts.map +1 -0
- package/dist/gepa-python-command.js +26 -0
- package/dist/gepa-python-paths.d.ts +6 -0
- package/dist/gepa-python-paths.d.ts.map +1 -0
- package/dist/gepa-python-paths.js +42 -0
- package/dist/git-credential.d.ts +69 -0
- package/dist/git-credential.d.ts.map +1 -0
- package/dist/git-recovery-environment.d.ts +31 -0
- package/dist/git-recovery-environment.d.ts.map +1 -0
- package/dist/github-setup.d.ts +122 -0
- package/dist/github-setup.d.ts.map +1 -0
- package/dist/global-flags.d.ts +12 -0
- package/dist/global-flags.d.ts.map +1 -0
- package/dist/help.d.ts +46 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +93 -25
- package/dist/hosted-boot.d.ts +273 -0
- package/dist/hosted-boot.d.ts.map +1 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
- package/dist/hosted-bootstrap.d.ts +188 -0
- package/dist/hosted-bootstrap.d.ts.map +1 -0
- package/dist/hosted-broker-server.d.ts +50 -0
- package/dist/hosted-broker-server.d.ts.map +1 -0
- package/dist/hosted-git-http-server.d.ts +36 -0
- package/dist/hosted-git-http-server.d.ts.map +1 -0
- package/dist/hosted-harness-claude.d.ts +195 -0
- package/dist/hosted-harness-claude.d.ts.map +1 -0
- package/dist/hosted-harness-opencode.d.ts +227 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -0
- package/dist/hosted-harness.d.ts +118 -0
- package/dist/hosted-harness.d.ts.map +1 -0
- package/dist/hosted-harvest.d.ts +68 -0
- package/dist/hosted-harvest.d.ts.map +1 -0
- package/dist/hosted-headless.d.ts +72 -0
- package/dist/hosted-headless.d.ts.map +1 -0
- package/dist/hosted-loop-lifecycle.d.ts +206 -0
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
- package/dist/hosted-loop.d.ts +170 -0
- package/dist/hosted-loop.d.ts.map +1 -0
- package/dist/hosted-run-event-sink.d.ts +222 -0
- package/dist/hosted-run-event-sink.d.ts.map +1 -0
- package/dist/hosted-runtime-assets.d.ts +186 -0
- package/dist/hosted-runtime-assets.d.ts.map +1 -0
- package/dist/hosted-session-cli.d.ts +258 -0
- package/dist/hosted-session-cli.d.ts.map +1 -0
- package/dist/hosted-skill-staging.d.ts +73 -0
- package/dist/hosted-skill-staging.d.ts.map +1 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -129
- package/dist/instruction-set-gepa-launch.d.ts +3 -0
- package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
- package/dist/instruction-set-gepa-launch.js +15 -0
- package/dist/instruction-set-loader.d.ts +6 -0
- package/dist/instruction-set-loader.d.ts.map +1 -0
- package/dist/instruction-set-loader.js +59 -0
- package/dist/instruction-set-manifest.d.ts +14 -0
- package/dist/instruction-set-manifest.d.ts.map +1 -0
- package/dist/instruction-set-manifest.js +162 -0
- package/dist/instruction-sets-cli.d.ts +40 -0
- package/dist/instruction-sets-cli.d.ts.map +1 -0
- package/dist/instruction-sets-cli.js +361 -0
- package/dist/json-response.d.ts +4 -0
- package/dist/json-response.d.ts.map +1 -0
- package/dist/jsonl-stream.d.ts +8 -0
- package/dist/jsonl-stream.d.ts.map +1 -0
- package/dist/kill-switch-cli.d.ts +30 -0
- package/dist/kill-switch-cli.d.ts.map +1 -0
- package/dist/loopback-serve.d.ts +25 -0
- package/dist/loopback-serve.d.ts.map +1 -0
- package/dist/manifests-cli.d.ts +22 -0
- package/dist/manifests-cli.d.ts.map +1 -0
- package/dist/markdown-report-input.d.ts +7 -0
- package/dist/markdown-report-input.d.ts.map +1 -0
- package/dist/markdown-report-input.js +15 -1
- package/dist/merge-job.d.ts +155 -0
- package/dist/merge-job.d.ts.map +1 -0
- package/dist/model-configs-cli.d.ts +19 -0
- package/dist/model-configs-cli.d.ts.map +1 -0
- package/dist/model-configs-cli.js +104 -0
- package/dist/optimization-export-cli.d.ts +19 -0
- package/dist/optimization-export-cli.d.ts.map +1 -0
- package/dist/optimizations-list-cli.d.ts +20 -0
- package/dist/optimizations-list-cli.d.ts.map +1 -0
- package/dist/optimizations-promote-cli.d.ts +8 -0
- package/dist/optimizations-promote-cli.d.ts.map +1 -0
- package/dist/optimizations-promote-cli.js +34 -0
- package/dist/option-validation.d.ts +15 -0
- package/dist/option-validation.d.ts.map +1 -0
- package/dist/preview-runtime.d.ts +63 -0
- package/dist/preview-runtime.d.ts.map +1 -0
- package/dist/prompt-draft-push.d.ts +60 -0
- package/dist/prompt-draft-push.d.ts.map +1 -0
- package/dist/prompt-label-cli.d.ts +11 -0
- package/dist/prompt-label-cli.d.ts.map +1 -0
- package/dist/prompt-label-cli.js +29 -0
- package/dist/prompt-length-wire.d.ts +28 -0
- package/dist/prompt-length-wire.d.ts.map +1 -0
- package/dist/prompt-length-wire.js +2 -2
- package/dist/prompt-report-cli.d.ts +10 -0
- package/dist/prompt-report-cli.d.ts.map +1 -0
- package/dist/prompt-report-cli.js +2 -2
- package/dist/prompt-summary-table.d.ts +15 -0
- package/dist/prompt-summary-table.d.ts.map +1 -0
- package/dist/prompt-summary-table.js +6 -0
- package/dist/registration-tag-warning.d.ts +32 -0
- package/dist/registration-tag-warning.d.ts.map +1 -0
- package/dist/report-comments-cli.d.ts +16 -0
- package/dist/report-comments-cli.d.ts.map +1 -0
- package/dist/report-markdown-blankness.d.ts +2 -0
- package/dist/report-markdown-blankness.d.ts.map +1 -0
- package/dist/report-markdown-blankness.js +6 -0
- package/dist/report-replacement-warning.d.ts +17 -0
- package/dist/report-replacement-warning.d.ts.map +1 -0
- package/dist/report-replacement-warning.js +31 -0
- package/dist/repository-backup-core.d.ts +225 -0
- package/dist/repository-backup-core.d.ts.map +1 -0
- package/dist/repository-recovery-manifest.d.ts +83 -0
- package/dist/repository-recovery-manifest.d.ts.map +1 -0
- package/dist/run-event-digest.d.ts +74 -0
- package/dist/run-event-digest.d.ts.map +1 -0
- package/dist/runner-dir-verify.d.ts +66 -0
- package/dist/runner-dir-verify.d.ts.map +1 -0
- package/dist/runner-instruction-set-contract.d.ts +24 -0
- package/dist/runner-instruction-set-contract.d.ts.map +1 -0
- package/dist/runner-instruction-set-contract.js +50 -0
- package/dist/sandbox-hygiene.d.ts +45 -0
- package/dist/sandbox-hygiene.d.ts.map +1 -0
- package/dist/sandbox-provider.d.ts +193 -0
- package/dist/sandbox-provider.d.ts.map +1 -0
- package/dist/scorer-draft-push.d.ts +50 -0
- package/dist/scorer-draft-push.d.ts.map +1 -0
- package/dist/scorers-cli.d.ts +20 -0
- package/dist/scorers-cli.d.ts.map +1 -0
- package/dist/scorers-cli.js +22 -0
- package/dist/scorers-verify-parity-bridge.d.ts +16 -0
- package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
- package/dist/scorers-verify-parity-bridge.js +90 -0
- package/dist/scorers-verify-parity.d.ts +81 -0
- package/dist/scorers-verify-parity.d.ts.map +1 -0
- package/dist/scorers-verify-parity.js +502 -0
- package/dist/scores-accept-cli.d.ts +21 -0
- package/dist/scores-accept-cli.d.ts.map +1 -0
- package/dist/secret-redaction.d.ts +47 -0
- package/dist/secret-redaction.d.ts.map +1 -0
- package/dist/session-timeline.d.ts +54 -0
- package/dist/session-timeline.d.ts.map +1 -0
- package/dist/setup-prompt.d.ts +5 -0
- package/dist/setup-prompt.d.ts.map +1 -0
- package/dist/skill-installer.d.ts +68 -0
- package/dist/skill-installer.d.ts.map +1 -0
- package/dist/skilled-proposer-launch.d.ts +7 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -0
- package/dist/skilled-proposer-launch.js +60 -0
- package/dist/smoke.d.ts +102 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/start-intent.d.ts +12 -0
- package/dist/start-intent.d.ts.map +1 -0
- package/dist/task-assignment-manifest.d.ts +15 -0
- package/dist/task-assignment-manifest.d.ts.map +1 -0
- package/dist/task-create-error.d.ts +18 -0
- package/dist/task-create-error.d.ts.map +1 -0
- package/dist/team-connectors-cli.d.ts +29 -0
- package/dist/team-connectors-cli.d.ts.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vercel-sandbox-provider.d.ts +156 -0
- package/dist/vercel-sandbox-provider.d.ts.map +1 -0
- package/dist/workbench-cli.d.ts +233 -0
- package/dist/workbench-cli.d.ts.map +1 -0
- package/dist/workspace-sync.d.ts +207 -0
- package/dist/workspace-sync.d.ts.map +1 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/zip-draft-push.d.ts +55 -0
- package/dist/zip-draft-push.d.ts.map +1 -0
- package/package.json +15 -2
- package/requirements/README.md +138 -0
- package/requirements/skilled-proposer.in +66 -0
- package/requirements/skilled-proposer.lock +265 -0
- package/scripts/build-cli-dist.mjs +30 -0
- package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
- package/scripts/orizu-source-checkout.d.mts +1 -0
- package/scripts/orizu-source-checkout.mjs +10 -0
- package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
- package/scripts/vendor-gepa-python.mjs +187 -4
- package/src/diff-comments-cli.ts +41 -3
- package/src/gepa-engine-dispatch.ts +340 -0
- package/src/gepa-python-command.ts +31 -0
- package/src/gepa-python-paths.ts +56 -0
- package/src/help.ts +95 -25
- package/src/index.ts +131 -166
- package/src/instruction-set-gepa-launch.ts +28 -0
- package/src/instruction-set-loader.ts +37 -0
- package/src/instruction-set-manifest.ts +120 -0
- package/src/instruction-sets-cli.ts +306 -0
- package/src/markdown-report-input.ts +19 -1
- package/src/model-configs-cli.ts +119 -0
- package/src/optimizations-promote-cli.ts +41 -0
- package/src/prompt-label-cli.ts +55 -0
- package/src/prompt-length-wire.ts +1 -1
- package/src/prompt-report-cli.ts +2 -2
- package/src/prompt-summary-table.ts +12 -0
- package/src/report-markdown-blankness.ts +6 -0
- package/src/report-replacement-warning.ts +35 -0
- package/src/runner-instruction-set-contract.ts +64 -0
- package/src/scorers-cli.ts +40 -0
- package/src/scorers-verify-parity-bridge.ts +90 -0
- package/src/scorers-verify-parity.ts +564 -0
- package/src/skilled-proposer-launch.ts +89 -0
- package/src/skilled-proposer-venv-manager.mjs +757 -0
- package/tsconfig.json +2 -0
- package/vendor/gepa-python/LICENSE +7 -0
- package/vendor/gepa-python/manifest.json +107 -0
- package/vendor/gepa-python/src/gepa/__init__.py +22 -0
- package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
- package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
- package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
- package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
- package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
- package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- package/vendor/gepa-python/src/gepa/api.py +468 -0
- package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
- package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
- package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
- package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
- package/vendor/gepa-python/src/gepa/core/result.py +271 -0
- package/vendor/gepa-python/src/gepa/core/state.py +706 -0
- package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
- package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
- package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
- package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
- package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
- package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
- package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
- package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
- package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
- package/vendor/gepa-python/src/gepa/image.py +104 -0
- package/vendor/gepa-python/src/gepa/lm.py +261 -0
- package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
- package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
- package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
- package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
- package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
- package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
- package/vendor/gepa-python/src/gepa/py.typed +0 -0
- package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
- package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
- package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
- package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
- package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
- package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
- package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
- package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
- package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
- package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
- package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
- package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
- package/vendor/gepa-python/src/gepa/visualization.py +359 -0
- package/vendor/orizu-gepa/pyproject.toml +18 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +9 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +138 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +666 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +269 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +186 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +721 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +55 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +203 -0
- package/vendor/orizu-gepa-python/pyproject.toml +1 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
- package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +52 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
- package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
- package/vendor/skills/orizu-cli/SKILL.md +3 -1
- package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
- package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +26 -1
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +113 -10
package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
"""GEPAAdapter implementation for the ``optimize_anything`` API.
|
|
2
|
+
|
|
3
|
+
This adapter bridges the user-facing ``optimize_anything`` API and GEPA's
|
|
4
|
+
internal engine. It handles:
|
|
5
|
+
|
|
6
|
+
- **Evaluation**: calls the user's wrapped evaluator (single or parallel)
|
|
7
|
+
- **Caching**: optional memory or disk cache for ``(candidate, example)`` pairs
|
|
8
|
+
- **Refinement**: when :class:`~gepa.optimize_anything.RefinerConfig` is set,
|
|
9
|
+
iteratively improves candidates via an LLM after each evaluation
|
|
10
|
+
- **Best-evals tracking**: maintains top-K evaluations per example for
|
|
11
|
+
warm-starting via :class:`~gepa.optimize_anything.OptimizationState`
|
|
12
|
+
- **Reflective dataset**: formats evaluation results for the reflection LLM
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import inspect
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import pickle
|
|
20
|
+
import threading
|
|
21
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
from gepa.core.adapter import DataInst, EvaluationBatch, GEPAAdapter
|
|
29
|
+
from gepa.proposer.reflective_mutation.base import LanguageModel
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from gepa.optimize_anything import Candidate, OptimizationState, RefinerConfig, SideInfo
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
REFINER_PROMPT_TEMPLATE = """You are refining a candidate to improve its performance.
|
|
36
|
+
|
|
37
|
+
## Instructions
|
|
38
|
+
{refiner_prompt}
|
|
39
|
+
|
|
40
|
+
## Current Candidate (JSON)
|
|
41
|
+
```json
|
|
42
|
+
{candidate_to_improve}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Evaluation History
|
|
46
|
+
The following shows all evaluation attempts so far, including scores and feedback:
|
|
47
|
+
```json
|
|
48
|
+
{evaluation_feedback}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Task
|
|
52
|
+
Analyze the evaluation history and propose an improved version of the candidate.
|
|
53
|
+
Return ONLY a valid JSON object with the improved parameters (no explanation, no markdown fences).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BatchEvaluatorWrapper:
|
|
58
|
+
"""Wraps a user ``batch_evaluator`` while preserving its single-external-call contract.
|
|
59
|
+
|
|
60
|
+
The whole point of ``batch_evaluator`` is that the user receives ALL
|
|
61
|
+
(candidate, example) pairs in ONE call (e.g. to submit a provider batch job
|
|
62
|
+
or fan out over their own cluster), so per-call features of the standard
|
|
63
|
+
evaluator path — ``oa.log()`` capture and stdout/stderr capture — cannot
|
|
64
|
+
apply here: there is no per-evaluation call to scope them to. Diagnostics
|
|
65
|
+
belong in each returned ``side_info``.
|
|
66
|
+
|
|
67
|
+
What IS restored for parity with the standard evaluator path:
|
|
68
|
+
|
|
69
|
+
- ``str_candidate_mode``: candidates are unwrapped to ``str`` before the
|
|
70
|
+
user function sees them (mirror of ``EvaluatorWrapper``).
|
|
71
|
+
- ``opt_states`` injection: if the function's signature accepts an
|
|
72
|
+
``opt_states`` keyword (or ``**kwargs``), it receives a list of
|
|
73
|
+
:class:`OptimizationState` aligned with ``pairs`` — the batch analogue of
|
|
74
|
+
the per-call ``opt_state`` kwarg (warm-starting, #160).
|
|
75
|
+
- Exception policy: when ``raise_on_exception=False``, a raised batch call
|
|
76
|
+
is converted to per-pair ``(0.0, {"error": ...})`` results instead of
|
|
77
|
+
sinking the iteration.
|
|
78
|
+
- Result normalization: each per-pair result may be ``float``,
|
|
79
|
+
``(score, side_info)`` (the mirror of the evaluator protocol), or a
|
|
80
|
+
legacy ``(score, output, side_info)`` 3-tuple; all normalize to
|
|
81
|
+
``(score, side_info)``.
|
|
82
|
+
|
|
83
|
+
Concurrency: grouped evaluations arrive as one call at a time, but when
|
|
84
|
+
``refiner_config`` is combined with ``parallel=True`` (and no single-pair
|
|
85
|
+
evaluator exists), per-example refinement loops run on a thread pool and
|
|
86
|
+
may call this wrapper concurrently with singleton batches — the wrapped
|
|
87
|
+
function should be thread-safe in that configuration.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
batch_fn: Callable[..., Any],
|
|
93
|
+
*,
|
|
94
|
+
str_candidate_key: str | None = None,
|
|
95
|
+
raise_on_exception: bool = True,
|
|
96
|
+
single_instance_mode: bool = False,
|
|
97
|
+
):
|
|
98
|
+
self._fn = batch_fn
|
|
99
|
+
self._str_candidate_key = str_candidate_key
|
|
100
|
+
self._raise_on_exception = raise_on_exception
|
|
101
|
+
self._single_instance_mode = single_instance_mode
|
|
102
|
+
sig = inspect.signature(batch_fn)
|
|
103
|
+
has_var_kw = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values())
|
|
104
|
+
self._accepts_opt_states = has_var_kw or "opt_states" in sig.parameters
|
|
105
|
+
|
|
106
|
+
def __call__(
|
|
107
|
+
self,
|
|
108
|
+
pairs: list[tuple["Candidate", Any]],
|
|
109
|
+
opt_states: "list[OptimizationState] | None" = None,
|
|
110
|
+
) -> list[tuple[float, dict[str, Any]]]:
|
|
111
|
+
call_pairs: list[tuple[Any, Any]] = list(pairs)
|
|
112
|
+
if self._str_candidate_key is not None:
|
|
113
|
+
call_pairs = [(candidate[self._str_candidate_key], example) for candidate, example in call_pairs]
|
|
114
|
+
if self._single_instance_mode:
|
|
115
|
+
# Mirror of EvaluatorWrapper's single-instance handling: the
|
|
116
|
+
# internal placeholder example must never reach user code — the
|
|
117
|
+
# user-visible example is None in this mode.
|
|
118
|
+
call_pairs = [(candidate, None) for candidate, _example in call_pairs]
|
|
119
|
+
|
|
120
|
+
kwargs: dict[str, Any] = {}
|
|
121
|
+
if self._accepts_opt_states and opt_states is not None:
|
|
122
|
+
kwargs["opt_states"] = opt_states
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
raw_results = list(self._fn(call_pairs, **kwargs))
|
|
126
|
+
except Exception as e:
|
|
127
|
+
if self._raise_on_exception:
|
|
128
|
+
raise
|
|
129
|
+
# _gepa_transient_failure marks these as synthetic whole-batch
|
|
130
|
+
# failure results: the adapter will NOT cache them, so one
|
|
131
|
+
# transient infrastructure error cannot poison the (disk) cache
|
|
132
|
+
# for every pair across runs.
|
|
133
|
+
return [(0.0, {"error": str(e), "_gepa_transient_failure": True}) for _ in pairs]
|
|
134
|
+
|
|
135
|
+
if len(raw_results) != len(pairs):
|
|
136
|
+
raise ValueError(f"batch_evaluator returned {len(raw_results)} results but expected {len(pairs)}")
|
|
137
|
+
|
|
138
|
+
normalized: list[tuple[float, dict[str, Any]]] = []
|
|
139
|
+
for idx, r in enumerate(raw_results):
|
|
140
|
+
if isinstance(r, list | tuple):
|
|
141
|
+
r_seq = list(r)
|
|
142
|
+
if len(r_seq) >= 3:
|
|
143
|
+
# Legacy (score, output, side_info): the output slot is
|
|
144
|
+
# ignored — the adapter packages outputs identically to the
|
|
145
|
+
# sequential path as (score, candidate, side_info).
|
|
146
|
+
si_raw = r_seq[2]
|
|
147
|
+
elif len(r_seq) == 2:
|
|
148
|
+
si_raw = r_seq[1]
|
|
149
|
+
else:
|
|
150
|
+
si_raw = {}
|
|
151
|
+
if si_raw is None:
|
|
152
|
+
si_raw = {}
|
|
153
|
+
if not isinstance(si_raw, dict):
|
|
154
|
+
# Mirror the evaluator path's loudness instead of silently
|
|
155
|
+
# discarding user diagnostics.
|
|
156
|
+
raise TypeError(
|
|
157
|
+
f"batch_evaluator result {idx}: side_info must be a dict or None, "
|
|
158
|
+
f"got {type(si_raw).__name__}"
|
|
159
|
+
)
|
|
160
|
+
# Defensive copy (mirror of EvaluatorWrapper): the user may
|
|
161
|
+
# retain and mutate their dict; the cache, best-evals history,
|
|
162
|
+
# and objective scores must not be corruptible after return.
|
|
163
|
+
normalized.append((float(r_seq[0]), dict(si_raw)))
|
|
164
|
+
else:
|
|
165
|
+
normalized.append((float(r), {}))
|
|
166
|
+
return normalized
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class OptimizeAnythingAdapter(GEPAAdapter):
|
|
170
|
+
"""Adapter connecting the ``optimize_anything`` API to GEPA's engine.
|
|
171
|
+
|
|
172
|
+
Created automatically by :func:`~gepa.optimize_anything.optimize_anything` —
|
|
173
|
+
users do not instantiate this directly.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
evaluator: Callable[..., tuple[float, Any, dict[str, Any]]] | None = None,
|
|
179
|
+
reflection_lm: LanguageModel | None = None,
|
|
180
|
+
reflection_prompt_template: str | None = None,
|
|
181
|
+
parallel: bool = True,
|
|
182
|
+
max_workers: int | None = None,
|
|
183
|
+
refiner_config: "RefinerConfig | None" = None,
|
|
184
|
+
best_example_evals_k: int = 1,
|
|
185
|
+
objective: str | None = None,
|
|
186
|
+
background: str | None = None,
|
|
187
|
+
cache_mode: str = "memory", # "off", "memory", "disk"
|
|
188
|
+
cache_dir: str | Path | None = None,
|
|
189
|
+
batch_evaluator: Callable[..., Any] | None = None,
|
|
190
|
+
):
|
|
191
|
+
if evaluator is None and batch_evaluator is None:
|
|
192
|
+
raise ValueError("Provide evaluator=, batch_evaluator=, or both.")
|
|
193
|
+
self.evaluator = evaluator
|
|
194
|
+
if batch_evaluator is not None and not isinstance(batch_evaluator, BatchEvaluatorWrapper):
|
|
195
|
+
batch_evaluator = BatchEvaluatorWrapper(batch_evaluator)
|
|
196
|
+
self._batch_evaluator = batch_evaluator
|
|
197
|
+
if batch_evaluator is not None and parallel:
|
|
198
|
+
logger.info(
|
|
199
|
+
"batch_evaluator provided: grouped evaluations route through it, so the "
|
|
200
|
+
"parallel=True thread pool is not used as their transport. If refiner_config "
|
|
201
|
+
"is set, refinement loops still run on the thread pool and may invoke "
|
|
202
|
+
"batch_evaluator concurrently with singleton batches — ensure it is thread-safe."
|
|
203
|
+
)
|
|
204
|
+
self.parallel = parallel
|
|
205
|
+
self.max_workers = max_workers
|
|
206
|
+
self.refiner_config = refiner_config
|
|
207
|
+
self.best_example_evals_k = best_example_evals_k
|
|
208
|
+
self.objective = objective
|
|
209
|
+
self.background = background
|
|
210
|
+
self.cache_mode = cache_mode
|
|
211
|
+
self.cache_dir = Path(cache_dir) if cache_dir else None
|
|
212
|
+
|
|
213
|
+
# Track top-K best evaluations per example for warm-start support
|
|
214
|
+
self._best_evals_by_example: dict[str, list[dict]] = {} # keyed by _example_hash
|
|
215
|
+
self._best_evals_lock = threading.Lock() # Thread safety for parallel execution
|
|
216
|
+
|
|
217
|
+
# Initialize evaluation cache
|
|
218
|
+
self._eval_cache: dict[tuple[str, str], tuple[float, Any, dict]] = {}
|
|
219
|
+
self._eval_cache_lock = threading.Lock()
|
|
220
|
+
|
|
221
|
+
# Setup disk cache directory and load existing entries
|
|
222
|
+
self._cache_dir_path: Path | None = None
|
|
223
|
+
if self.cache_mode == "disk" and self.cache_dir:
|
|
224
|
+
self._cache_dir_path = self.cache_dir / "fitness_cache"
|
|
225
|
+
self._cache_dir_path.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
self._load_cache()
|
|
227
|
+
|
|
228
|
+
# Refiner uses LiteLLM directly via refiner_config.refiner_lm callable
|
|
229
|
+
# No separate predictor needed - the LM callable is set up in optimize_anything.py
|
|
230
|
+
|
|
231
|
+
def _get_best_example_evals(self, example: object) -> list[dict]:
|
|
232
|
+
"""Get sorted top-K best evaluations for this example (thread-safe)."""
|
|
233
|
+
key = self._example_hash(example)
|
|
234
|
+
with self._best_evals_lock:
|
|
235
|
+
# Return a copy to avoid mutation issues
|
|
236
|
+
return list(self._best_evals_by_example.get(key, []))
|
|
237
|
+
|
|
238
|
+
def _update_best_example_evals(self, example: object, score: float, side_info: "SideInfo") -> None:
|
|
239
|
+
"""Add evaluation to example's best evals, maintain sorted top-K (thread-safe)."""
|
|
240
|
+
key = self._example_hash(example)
|
|
241
|
+
with self._best_evals_lock:
|
|
242
|
+
if key not in self._best_evals_by_example:
|
|
243
|
+
self._best_evals_by_example[key] = []
|
|
244
|
+
|
|
245
|
+
self._best_evals_by_example[key].append({"score": score, "side_info": side_info})
|
|
246
|
+
|
|
247
|
+
# Sort descending by score, keep top K
|
|
248
|
+
self._best_evals_by_example[key] = sorted(
|
|
249
|
+
self._best_evals_by_example[key],
|
|
250
|
+
key=lambda x: x["score"],
|
|
251
|
+
reverse=True,
|
|
252
|
+
)[: self.best_example_evals_k]
|
|
253
|
+
|
|
254
|
+
# --- Evaluation caching methods ---
|
|
255
|
+
|
|
256
|
+
def _candidate_hash(self, candidate: dict[str, str]) -> str:
|
|
257
|
+
"""SHA256 hash of candidate for cache key (first 16 chars)."""
|
|
258
|
+
return hashlib.sha256(json.dumps(sorted(candidate.items())).encode()).hexdigest()[:16]
|
|
259
|
+
|
|
260
|
+
def _example_hash(self, example: Any) -> str:
|
|
261
|
+
"""Hash example for cache key (first 16 chars)."""
|
|
262
|
+
if example is None:
|
|
263
|
+
return "none"
|
|
264
|
+
try:
|
|
265
|
+
# Try JSON serialization for dicts, lists, primitives
|
|
266
|
+
return hashlib.sha256(json.dumps(example, sort_keys=True, default=str).encode()).hexdigest()[:16]
|
|
267
|
+
except (TypeError, ValueError):
|
|
268
|
+
# Fallback to id for unhashable objects
|
|
269
|
+
return f"id_{id(example)}"
|
|
270
|
+
|
|
271
|
+
def _cache_key(self, candidate: dict[str, str], example: Any) -> tuple[str, str]:
|
|
272
|
+
"""Build cache key tuple."""
|
|
273
|
+
return (self._candidate_hash(candidate), self._example_hash(example))
|
|
274
|
+
|
|
275
|
+
def _cache_filename(self, cache_key: tuple[str, str]) -> str:
|
|
276
|
+
"""Build filename from cache key."""
|
|
277
|
+
return f"{cache_key[0]}_{cache_key[1]}.pkl"
|
|
278
|
+
|
|
279
|
+
def _load_cache(self) -> None:
|
|
280
|
+
"""Load all cache entries from disk into memory."""
|
|
281
|
+
if self._cache_dir_path is None:
|
|
282
|
+
return
|
|
283
|
+
for cache_file in self._cache_dir_path.glob("*.pkl"):
|
|
284
|
+
try:
|
|
285
|
+
with open(cache_file, "rb") as f:
|
|
286
|
+
data = pickle.load(f)
|
|
287
|
+
# File stores: {"key": (cand_hash, ex_hash), "result": (score, output, side_info)}
|
|
288
|
+
self._eval_cache[data["key"]] = data["result"]
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
logger.warning("Failed to load cache file %s: %s", cache_file, exc)
|
|
291
|
+
|
|
292
|
+
def _save_cache_entry(self, cache_key: tuple[str, str], result: tuple) -> None:
|
|
293
|
+
"""Save single cache entry to disk."""
|
|
294
|
+
if self._cache_dir_path is None:
|
|
295
|
+
return
|
|
296
|
+
filename = self._cache_filename(cache_key)
|
|
297
|
+
filepath = self._cache_dir_path / filename
|
|
298
|
+
with open(filepath, "wb") as f:
|
|
299
|
+
pickle.dump({"key": cache_key, "result": result}, f)
|
|
300
|
+
|
|
301
|
+
def _build_opt_state(self, example: Any) -> "OptimizationState":
|
|
302
|
+
"""Build an OptimizationState for the given example."""
|
|
303
|
+
from gepa.optimize_anything import OptimizationState
|
|
304
|
+
|
|
305
|
+
return OptimizationState(best_example_evals=self._get_best_example_evals(example))
|
|
306
|
+
|
|
307
|
+
def _resolve_pair(self, candidate: "Candidate", example: Any) -> tuple[float, Any, dict]:
|
|
308
|
+
"""Resolve ONE (candidate, example) pair, whichever evaluation transport exists.
|
|
309
|
+
|
|
310
|
+
Uses the single-pair evaluator when provided; otherwise routes a
|
|
311
|
+
singleton batch through ``batch_evaluator``. This is the seam that
|
|
312
|
+
makes the refiner (and any other per-pair resolution) work identically
|
|
313
|
+
with or without a single-pair evaluator — including shared caching,
|
|
314
|
+
since :meth:`_call_evaluator`'s cache wraps this method on both paths.
|
|
315
|
+
"""
|
|
316
|
+
if self.evaluator is not None:
|
|
317
|
+
return self.evaluator(
|
|
318
|
+
candidate,
|
|
319
|
+
example=example,
|
|
320
|
+
opt_state=self._build_opt_state(example),
|
|
321
|
+
)
|
|
322
|
+
assert self._batch_evaluator is not None
|
|
323
|
+
((score, side_info),) = self._batch_evaluator(
|
|
324
|
+
[(candidate, example)], opt_states=[self._build_opt_state(example)]
|
|
325
|
+
)
|
|
326
|
+
# Slot 1 is the "output" placeholder, mirroring EvaluatorWrapper's
|
|
327
|
+
# (score, None, side_info) contract; downstream packaging rebuilds it
|
|
328
|
+
# from the candidate (_assemble_no_refiner_batch / refiner path).
|
|
329
|
+
return score, None, side_info
|
|
330
|
+
|
|
331
|
+
def _call_evaluator(
|
|
332
|
+
self,
|
|
333
|
+
candidate: "Candidate",
|
|
334
|
+
example: Any,
|
|
335
|
+
) -> tuple[float, Any, dict]:
|
|
336
|
+
"""Call evaluator with optional caching."""
|
|
337
|
+
# No caching
|
|
338
|
+
if self.cache_mode == "off":
|
|
339
|
+
return self._resolve_pair(candidate, example)
|
|
340
|
+
|
|
341
|
+
# Build cache key
|
|
342
|
+
cache_key = self._cache_key(candidate, example)
|
|
343
|
+
|
|
344
|
+
# Check cache (thread-safe)
|
|
345
|
+
with self._eval_cache_lock:
|
|
346
|
+
if cache_key in self._eval_cache:
|
|
347
|
+
return self._eval_cache[cache_key]
|
|
348
|
+
|
|
349
|
+
# Cache miss - call evaluator
|
|
350
|
+
result = self._resolve_pair(candidate, example)
|
|
351
|
+
|
|
352
|
+
# Store in cache (thread-safe); synthetic transient-failure results
|
|
353
|
+
# from a batch_evaluator exception are never cached.
|
|
354
|
+
side_info = result[2] if len(result) > 2 and isinstance(result[2], dict) else {}
|
|
355
|
+
if not side_info.get("_gepa_transient_failure"):
|
|
356
|
+
with self._eval_cache_lock:
|
|
357
|
+
self._eval_cache[cache_key] = result
|
|
358
|
+
if self.cache_mode == "disk":
|
|
359
|
+
self._save_cache_entry(cache_key, result)
|
|
360
|
+
|
|
361
|
+
return result
|
|
362
|
+
|
|
363
|
+
def evaluate(
|
|
364
|
+
self,
|
|
365
|
+
batch: list[DataInst],
|
|
366
|
+
candidate: "Candidate",
|
|
367
|
+
capture_traces: bool = False,
|
|
368
|
+
) -> EvaluationBatch:
|
|
369
|
+
"""Evaluate a candidate on a batch of examples, with optional refinement.
|
|
370
|
+
|
|
371
|
+
When ``refiner_config`` is set, each example goes through an
|
|
372
|
+
evaluate→refine→re-evaluate loop before returning the best result.
|
|
373
|
+
Multi-objective scores from ``side_info["scores"]`` are extracted and
|
|
374
|
+
forwarded as ``objective_scores`` in the returned batch.
|
|
375
|
+
"""
|
|
376
|
+
# No-refiner path: evaluate each example (optionally in parallel) and
|
|
377
|
+
# package. Factored into ``_assemble_no_refiner_batch`` so that
|
|
378
|
+
# ``batch_evaluate``'s parallel fallback can reuse the exact same
|
|
379
|
+
# packaging (best-evals update, objective-score extraction, counts).
|
|
380
|
+
if self.refiner_config is None:
|
|
381
|
+
if self._batch_evaluator is not None:
|
|
382
|
+
# batch_evaluator is the preferred transport for any multi-pair
|
|
383
|
+
# evaluation (valset evals, merge evals, seed evals) — one
|
|
384
|
+
# external call for the whole batch, cache-aware.
|
|
385
|
+
return self._batch_evaluate_via_user_fn([(candidate, batch)])[0]
|
|
386
|
+
if self.parallel and len(batch) > 1:
|
|
387
|
+
raw_results = self._evaluate_parallel(batch, candidate)
|
|
388
|
+
else:
|
|
389
|
+
raw_results = [self._call_evaluator(candidate, example) for example in batch]
|
|
390
|
+
return self._assemble_no_refiner_batch(candidate, batch, raw_results)
|
|
391
|
+
|
|
392
|
+
# Refiner path: evaluate with refinement. eval_output is a list of
|
|
393
|
+
# (score, output, side_info) where output = (score, best_candidate, side_info).
|
|
394
|
+
eval_output = self._evaluate_with_refinement(batch, candidate)
|
|
395
|
+
|
|
396
|
+
scores = [score for score, _, _ in eval_output]
|
|
397
|
+
side_infos: list[SideInfo] = [info for _, _, info in eval_output]
|
|
398
|
+
# outputs = list of (score, best_candidate, side_info) tuples
|
|
399
|
+
outputs = [out for _, out, _ in eval_output]
|
|
400
|
+
objective_scores = [self._extract_objective_scores(candidate, si) for si in side_infos]
|
|
401
|
+
|
|
402
|
+
# Count actual evaluator invocations from the attempt history recorded
|
|
403
|
+
# in each side_info (the refiner path may call the evaluator multiple
|
|
404
|
+
# times per example).
|
|
405
|
+
num_metric_calls = 0
|
|
406
|
+
for si in side_infos:
|
|
407
|
+
rp_info = si.get("refiner_prompt_specific_info", {})
|
|
408
|
+
attempts = rp_info.get("Attempts", [])
|
|
409
|
+
num_metric_calls += sum(1 for a in attempts if "side_info" in a)
|
|
410
|
+
|
|
411
|
+
return EvaluationBatch(
|
|
412
|
+
outputs=outputs,
|
|
413
|
+
scores=scores,
|
|
414
|
+
trajectories=side_infos,
|
|
415
|
+
objective_scores=objective_scores,
|
|
416
|
+
num_metric_calls=num_metric_calls,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def batch_evaluate(
|
|
420
|
+
self,
|
|
421
|
+
items: list[tuple["Candidate", list]],
|
|
422
|
+
) -> list[EvaluationBatch]:
|
|
423
|
+
"""Evaluate multiple (candidate, batch) pairs.
|
|
424
|
+
|
|
425
|
+
When a ``batch_evaluator`` was provided at construction time (and no
|
|
426
|
+
refiner is configured), all (candidate, example) pairs are flattened
|
|
427
|
+
into a single call to the user's batch function.
|
|
428
|
+
|
|
429
|
+
Falls back to sequential ``evaluate()`` calls for the refiner path or a
|
|
430
|
+
single item. Always captures traces. With ``refiner_config`` set,
|
|
431
|
+
evaluation is per-example by construction (each example runs its own
|
|
432
|
+
evaluate->refine->re-evaluate loop), so grouped calls degrade to
|
|
433
|
+
singleton batches through ``_resolve_pair`` — refinement is never
|
|
434
|
+
silently skipped.
|
|
435
|
+
"""
|
|
436
|
+
if self._batch_evaluator is not None and self.refiner_config is None:
|
|
437
|
+
return self._batch_evaluate_via_user_fn(items)
|
|
438
|
+
if self.parallel and self.refiner_config is None and len(items) > 1:
|
|
439
|
+
return self._batch_evaluate_parallel_fallback(items)
|
|
440
|
+
return [self.evaluate(batch, candidate, capture_traces=True) for candidate, batch in items]
|
|
441
|
+
|
|
442
|
+
def _batch_evaluate_via_user_fn(
|
|
443
|
+
self,
|
|
444
|
+
items: list[tuple["Candidate", list]],
|
|
445
|
+
) -> list[EvaluationBatch]:
|
|
446
|
+
"""Flatten all (candidate, example) pairs, call batch_evaluator once, repackage.
|
|
447
|
+
|
|
448
|
+
The (wrapped) batch_evaluator normalizes every per-pair result to
|
|
449
|
+
``(score, side_info)``; packaging then goes through
|
|
450
|
+
:meth:`_assemble_no_refiner_batch` so the batch path produces byte-identical
|
|
451
|
+
EvaluationBatches to the sequential path: ``outputs`` are
|
|
452
|
+
``(score, candidate, side_info)`` tuples, best-evals history is updated
|
|
453
|
+
(feeding ``OptimizationState.best_example_evals``), objective scores are
|
|
454
|
+
extracted, and metric calls are counted the same way.
|
|
455
|
+
"""
|
|
456
|
+
assert self._batch_evaluator is not None
|
|
457
|
+
|
|
458
|
+
pairs: list[tuple[dict[str, str], Any]] = []
|
|
459
|
+
pair_to_item_idx: list[int] = []
|
|
460
|
+
for item_idx, (candidate, batch) in enumerate(items):
|
|
461
|
+
for example in batch:
|
|
462
|
+
pairs.append((candidate, example))
|
|
463
|
+
pair_to_item_idx.append(item_idx)
|
|
464
|
+
|
|
465
|
+
# Cache layer: identical key/value format to _call_evaluator, so
|
|
466
|
+
# entries are shared across the batch path, the sequential path, and
|
|
467
|
+
# the refiner's singleton resolutions. Only misses reach the user's
|
|
468
|
+
# function — still as ONE call.
|
|
469
|
+
results: list[tuple[float, dict[str, Any]] | None] = [None] * len(pairs)
|
|
470
|
+
miss_indices = list(range(len(pairs)))
|
|
471
|
+
if self.cache_mode != "off":
|
|
472
|
+
miss_indices = []
|
|
473
|
+
with self._eval_cache_lock:
|
|
474
|
+
for idx, (cand, example) in enumerate(pairs):
|
|
475
|
+
cached = self._eval_cache.get(self._cache_key(cand, example))
|
|
476
|
+
if cached is not None:
|
|
477
|
+
score, _output, side_info = cached
|
|
478
|
+
results[idx] = (score, side_info)
|
|
479
|
+
else:
|
|
480
|
+
miss_indices.append(idx)
|
|
481
|
+
|
|
482
|
+
if miss_indices:
|
|
483
|
+
# With caching enabled, duplicate (candidate, example) pairs within
|
|
484
|
+
# one grouped call resolve to a single user-fn evaluation (the
|
|
485
|
+
# sequential path gets the same effect through per-pair cache hits).
|
|
486
|
+
if self.cache_mode != "off":
|
|
487
|
+
first_by_key: dict[tuple[str, str], int] = {}
|
|
488
|
+
unique_miss: list[int] = []
|
|
489
|
+
alias_of: dict[int, int] = {}
|
|
490
|
+
for idx in miss_indices:
|
|
491
|
+
key = self._cache_key(*pairs[idx])
|
|
492
|
+
if key in first_by_key:
|
|
493
|
+
alias_of[idx] = first_by_key[key]
|
|
494
|
+
else:
|
|
495
|
+
first_by_key[key] = idx
|
|
496
|
+
unique_miss.append(idx)
|
|
497
|
+
else:
|
|
498
|
+
unique_miss = list(miss_indices)
|
|
499
|
+
alias_of = {}
|
|
500
|
+
|
|
501
|
+
miss_pairs = [pairs[idx] for idx in unique_miss]
|
|
502
|
+
miss_opt_states = [self._build_opt_state(example) for _, example in miss_pairs]
|
|
503
|
+
miss_results = self._batch_evaluator(miss_pairs, opt_states=miss_opt_states)
|
|
504
|
+
for idx, res in zip(unique_miss, miss_results, strict=True):
|
|
505
|
+
results[idx] = res
|
|
506
|
+
score, side_info = res
|
|
507
|
+
# Transient whole-batch failures (see BatchEvaluatorWrapper)
|
|
508
|
+
# are never cached — they must be re-evaluated next time.
|
|
509
|
+
if self.cache_mode != "off" and not side_info.get("_gepa_transient_failure"):
|
|
510
|
+
cache_key = self._cache_key(*pairs[idx])
|
|
511
|
+
with self._eval_cache_lock:
|
|
512
|
+
self._eval_cache[cache_key] = (score, None, side_info)
|
|
513
|
+
if self.cache_mode == "disk":
|
|
514
|
+
self._save_cache_entry(cache_key, (score, None, side_info))
|
|
515
|
+
for idx, src in alias_of.items():
|
|
516
|
+
results[idx] = results[src]
|
|
517
|
+
|
|
518
|
+
raw_by_item: dict[int, list[tuple[float, Any, SideInfo]]] = {}
|
|
519
|
+
for res, item_idx in zip(results, pair_to_item_idx, strict=True):
|
|
520
|
+
assert res is not None, "unresolved evaluation pair (cache/miss bookkeeping bug)"
|
|
521
|
+
score, side_info = res
|
|
522
|
+
raw_by_item.setdefault(item_idx, []).append((score, None, side_info))
|
|
523
|
+
|
|
524
|
+
return [
|
|
525
|
+
self._assemble_no_refiner_batch(items[item_idx][0], items[item_idx][1], raw_by_item.get(item_idx, []))
|
|
526
|
+
for item_idx in range(len(items))
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
def _evaluate_with_refinement(
|
|
530
|
+
self,
|
|
531
|
+
batch: list[DataInst],
|
|
532
|
+
candidate: "Candidate",
|
|
533
|
+
) -> list[tuple[float, Any, "SideInfo"]]:
|
|
534
|
+
"""Evaluate batch with automatic refinement."""
|
|
535
|
+
if self.parallel and len(batch) > 1:
|
|
536
|
+
return self._evaluate_with_refinement_parallel(batch, candidate)
|
|
537
|
+
|
|
538
|
+
results = []
|
|
539
|
+
for example in batch:
|
|
540
|
+
result = self._evaluate_single_with_refinement(candidate, example)
|
|
541
|
+
results.append(result)
|
|
542
|
+
return results
|
|
543
|
+
|
|
544
|
+
def _evaluate_single_with_refinement(
|
|
545
|
+
self,
|
|
546
|
+
candidate: "Candidate",
|
|
547
|
+
example: object,
|
|
548
|
+
) -> tuple[float, Any, "SideInfo"]:
|
|
549
|
+
"""Evaluate a single example with refinement."""
|
|
550
|
+
assert self.refiner_config is not None
|
|
551
|
+
|
|
552
|
+
# refiner_prompt is always in candidate (auto-injected by optimize_anything)
|
|
553
|
+
refiner_prompt = candidate.get("refiner_prompt", "")
|
|
554
|
+
|
|
555
|
+
# 1. Evaluate original candidate
|
|
556
|
+
original_score, original_output, original_side_info = self._call_evaluator(candidate, example)
|
|
557
|
+
|
|
558
|
+
# Update best evals with original evaluation
|
|
559
|
+
self._update_best_example_evals(example, original_score, original_side_info)
|
|
560
|
+
|
|
561
|
+
# 2. Refine and evaluate
|
|
562
|
+
best_refined_score, best_refined_candidate, best_refined_side_info, all_attempts = self._refine_and_evaluate(
|
|
563
|
+
candidate, example, refiner_prompt, original_score, original_side_info
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# 3. Score = best of original and refined (refiner is a score booster)
|
|
567
|
+
# Track best_candidate (the actual candidate dict that won)
|
|
568
|
+
if best_refined_score > original_score:
|
|
569
|
+
final_score = best_refined_score
|
|
570
|
+
best_candidate = best_refined_candidate # Refined candidate won
|
|
571
|
+
else:
|
|
572
|
+
final_score = original_score
|
|
573
|
+
best_candidate = candidate # Original candidate won
|
|
574
|
+
|
|
575
|
+
# 4. Side_info = original evaluation (so reflection sees raw candidate quality)
|
|
576
|
+
aggregated_side_info = dict(original_side_info)
|
|
577
|
+
|
|
578
|
+
# 5. Add refiner_prompt_specific_info with attempt history
|
|
579
|
+
# Only include "scores" if the user's side_info has "scores" (for objective frontier)
|
|
580
|
+
refiner_side_info: dict[str, Any] = {"Attempts": all_attempts}
|
|
581
|
+
|
|
582
|
+
if "scores" in original_side_info:
|
|
583
|
+
# Only consider attempts that produced real evaluation results (have "side_info")
|
|
584
|
+
# so that failed attempts (JSON parse errors, exceptions) with placeholder score=0.0
|
|
585
|
+
# don't mask real evaluations when original scores are negative
|
|
586
|
+
evaluated_attempts = [a for a in all_attempts if "side_info" in a]
|
|
587
|
+
best_refined_scores = {}
|
|
588
|
+
if evaluated_attempts:
|
|
589
|
+
best_attempt = max(
|
|
590
|
+
evaluated_attempts,
|
|
591
|
+
key=lambda x: x.get("score", float("-inf")),
|
|
592
|
+
)
|
|
593
|
+
best_attempt_side_info = best_attempt.get("side_info")
|
|
594
|
+
if isinstance(best_attempt_side_info, dict) and "scores" in best_attempt_side_info:
|
|
595
|
+
best_refined_scores = best_attempt_side_info["scores"]
|
|
596
|
+
refiner_side_info["scores"] = best_refined_scores
|
|
597
|
+
|
|
598
|
+
aggregated_side_info["refiner_prompt_specific_info"] = refiner_side_info
|
|
599
|
+
|
|
600
|
+
# Package output as (score, best_candidate, side_info) tuple
|
|
601
|
+
output = (final_score, best_candidate, aggregated_side_info)
|
|
602
|
+
return final_score, output, aggregated_side_info
|
|
603
|
+
|
|
604
|
+
def _run_parallel(
|
|
605
|
+
self,
|
|
606
|
+
batch: list[DataInst],
|
|
607
|
+
candidate: "Candidate",
|
|
608
|
+
eval_fn, # callable(candidate, example) -> result
|
|
609
|
+
) -> list[tuple[float, Any, "SideInfo"]]:
|
|
610
|
+
"""Run evaluation function in parallel across batch."""
|
|
611
|
+
results: list[tuple[int, tuple[float, Any, SideInfo]]] = []
|
|
612
|
+
|
|
613
|
+
with ThreadPoolExecutor(max_workers=self.max_workers or len(batch)) as executor:
|
|
614
|
+
future_to_idx = {executor.submit(eval_fn, candidate, example): idx for idx, example in enumerate(batch)}
|
|
615
|
+
for future in as_completed(future_to_idx):
|
|
616
|
+
idx = future_to_idx[future]
|
|
617
|
+
results.append((idx, future.result()))
|
|
618
|
+
|
|
619
|
+
results.sort(key=lambda x: x[0])
|
|
620
|
+
return [result for _, result in results]
|
|
621
|
+
|
|
622
|
+
def _run_parallel_pairs(
|
|
623
|
+
self,
|
|
624
|
+
pairs: list[tuple["Candidate", Any]],
|
|
625
|
+
) -> list[tuple[float, Any, "SideInfo"]]:
|
|
626
|
+
"""Evaluate ``(candidate, example)`` pairs concurrently, preserving input order.
|
|
627
|
+
|
|
628
|
+
Unlike :meth:`_run_parallel` (one candidate, many examples), this fans
|
|
629
|
+
out over pairs whose candidate may differ, so a single thread pool is
|
|
630
|
+
shared across all candidates in a ``batch_evaluate`` call.
|
|
631
|
+
"""
|
|
632
|
+
results: list[tuple[int, tuple[float, Any, SideInfo]]] = []
|
|
633
|
+
with ThreadPoolExecutor(max_workers=self.max_workers or len(pairs)) as executor:
|
|
634
|
+
future_to_idx = {
|
|
635
|
+
executor.submit(self._call_evaluator, candidate, example): idx
|
|
636
|
+
for idx, (candidate, example) in enumerate(pairs)
|
|
637
|
+
}
|
|
638
|
+
for future in as_completed(future_to_idx):
|
|
639
|
+
idx = future_to_idx[future]
|
|
640
|
+
results.append((idx, future.result()))
|
|
641
|
+
results.sort(key=lambda x: x[0])
|
|
642
|
+
return [result for _, result in results]
|
|
643
|
+
|
|
644
|
+
def _batch_evaluate_parallel_fallback(
|
|
645
|
+
self,
|
|
646
|
+
items: list[tuple["Candidate", list]],
|
|
647
|
+
) -> list[EvaluationBatch]:
|
|
648
|
+
"""Fan all ``(candidate, example)`` pairs out across one thread pool, regroup per item.
|
|
649
|
+
|
|
650
|
+
No-refiner path only. Saturates ``max_workers`` across candidates and
|
|
651
|
+
examples, then repackages each item via :meth:`_assemble_no_refiner_batch`
|
|
652
|
+
so results are identical to per-item ``evaluate()`` calls.
|
|
653
|
+
"""
|
|
654
|
+
pairs: list[tuple[Candidate, Any]] = []
|
|
655
|
+
pair_to_item_idx: list[int] = []
|
|
656
|
+
for item_idx, (candidate, batch) in enumerate(items):
|
|
657
|
+
for example in batch:
|
|
658
|
+
pairs.append((candidate, example))
|
|
659
|
+
pair_to_item_idx.append(item_idx)
|
|
660
|
+
|
|
661
|
+
raw_by_pair = self._run_parallel_pairs(pairs) if pairs else []
|
|
662
|
+
|
|
663
|
+
raw_per_item: list[list[tuple[float, Any, SideInfo]]] = [[] for _ in items]
|
|
664
|
+
for pair_idx, raw in enumerate(raw_by_pair):
|
|
665
|
+
raw_per_item[pair_to_item_idx[pair_idx]].append(raw)
|
|
666
|
+
|
|
667
|
+
return [
|
|
668
|
+
self._assemble_no_refiner_batch(candidate, batch, raw_per_item[item_idx])
|
|
669
|
+
for item_idx, (candidate, batch) in enumerate(items)
|
|
670
|
+
]
|
|
671
|
+
|
|
672
|
+
def _extract_objective_scores(self, candidate: "Candidate", side_info: "SideInfo") -> dict[str, float]:
|
|
673
|
+
"""Pull per-objective scores out of a side_info dict (top-level + per-param)."""
|
|
674
|
+
objective_score: dict[str, float] = {}
|
|
675
|
+
if "scores" in side_info:
|
|
676
|
+
objective_score.update(side_info["scores"])
|
|
677
|
+
for param_name in candidate.keys():
|
|
678
|
+
specific = side_info.get(param_name + "_specific_info") if isinstance(side_info, dict) else None
|
|
679
|
+
if isinstance(specific, dict) and "scores" in specific:
|
|
680
|
+
objective_score.update({f"{param_name}::{k}": v for k, v in specific["scores"].items()})
|
|
681
|
+
return objective_score
|
|
682
|
+
|
|
683
|
+
def _assemble_no_refiner_batch(
|
|
684
|
+
self,
|
|
685
|
+
candidate: "Candidate",
|
|
686
|
+
batch: list[DataInst],
|
|
687
|
+
raw_results: list[tuple[float, Any, "SideInfo"]],
|
|
688
|
+
) -> EvaluationBatch:
|
|
689
|
+
"""Package raw ``(score, _, side_info)`` evaluator results into an EvaluationBatch.
|
|
690
|
+
|
|
691
|
+
Shared by :meth:`evaluate` (no-refiner path) and
|
|
692
|
+
:meth:`_batch_evaluate_parallel_fallback` so both produce identical
|
|
693
|
+
output, update best-evals history, and count metric calls the same way.
|
|
694
|
+
"""
|
|
695
|
+
# Package outputs as (score, candidate, side_info) tuples.
|
|
696
|
+
eval_output = [(score, (score, candidate, side_info), side_info) for score, _, side_info in raw_results]
|
|
697
|
+
for example, (score, _, side_info) in zip(batch, eval_output, strict=True):
|
|
698
|
+
self._update_best_example_evals(example, score, side_info)
|
|
699
|
+
|
|
700
|
+
scores = [score for score, _, _ in eval_output]
|
|
701
|
+
side_infos: list[SideInfo] = [info for _, _, info in eval_output]
|
|
702
|
+
outputs = [out for _, out, _ in eval_output]
|
|
703
|
+
objective_scores = [self._extract_objective_scores(candidate, si) for si in side_infos]
|
|
704
|
+
|
|
705
|
+
return EvaluationBatch(
|
|
706
|
+
outputs=outputs,
|
|
707
|
+
scores=scores,
|
|
708
|
+
trajectories=side_infos,
|
|
709
|
+
objective_scores=objective_scores,
|
|
710
|
+
num_metric_calls=len(batch),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def _evaluate_parallel(self, batch, candidate):
|
|
714
|
+
"""Evaluate batch in parallel (no refinement)."""
|
|
715
|
+
return self._run_parallel(batch, candidate, self._call_evaluator)
|
|
716
|
+
|
|
717
|
+
def _evaluate_with_refinement_parallel(self, batch, candidate):
|
|
718
|
+
"""Evaluate batch in parallel with refinement."""
|
|
719
|
+
return self._run_parallel(batch, candidate, self._evaluate_single_with_refinement)
|
|
720
|
+
|
|
721
|
+
def _refine_and_evaluate(
|
|
722
|
+
self,
|
|
723
|
+
candidate: "Candidate",
|
|
724
|
+
example: object,
|
|
725
|
+
refiner_prompt: str,
|
|
726
|
+
original_score: float,
|
|
727
|
+
original_side_info: "SideInfo",
|
|
728
|
+
) -> tuple[float, dict[str, str] | None, "SideInfo | None", list[dict]]:
|
|
729
|
+
"""
|
|
730
|
+
Refine a candidate using the refiner LLM and evaluate the refined version.
|
|
731
|
+
Refines ALL non-refiner params at once via JSON dict.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
(best_refined_score, best_refined_candidate, best_refined_side_info, all_attempts)
|
|
735
|
+
best_refined_candidate is the candidate dict that achieved best refined score,
|
|
736
|
+
or None if no refinement improved over original.
|
|
737
|
+
best_refined_side_info is None if no refinement improved over original.
|
|
738
|
+
"""
|
|
739
|
+
# This method should only be called when refiner_config is not None
|
|
740
|
+
assert self.refiner_config is not None, "refiner_config must be set to use refinement"
|
|
741
|
+
|
|
742
|
+
# Build params dict: all non-refiner params
|
|
743
|
+
params_dict = {k: v for k, v in candidate.items() if k != "refiner_prompt"}
|
|
744
|
+
|
|
745
|
+
best_score = original_score
|
|
746
|
+
best_candidate: dict[str, str] | None = None
|
|
747
|
+
best_side_info: SideInfo | None = None
|
|
748
|
+
|
|
749
|
+
# Initialize attempts with original evaluation (iteration 0)
|
|
750
|
+
all_attempts: list[dict] = [
|
|
751
|
+
{
|
|
752
|
+
"iteration": 0,
|
|
753
|
+
"candidate": params_dict,
|
|
754
|
+
"score": original_score,
|
|
755
|
+
"side_info": original_side_info,
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
# Iterative refinement
|
|
760
|
+
refiner_lm = self.refiner_config.refiner_lm
|
|
761
|
+
assert callable(refiner_lm), (
|
|
762
|
+
"refiner_lm must be a callable LanguageModel, not a string or None. "
|
|
763
|
+
"Ensure optimize_anything() has converted it via make_litellm_lm()."
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
current_params = params_dict
|
|
767
|
+
for refinement_iter in range(self.refiner_config.max_refinements):
|
|
768
|
+
# Format ALL attempts so far for the refiner (provides full history)
|
|
769
|
+
current_feedback = self._format_all_attempts_feedback(all_attempts)
|
|
770
|
+
try:
|
|
771
|
+
# Call refiner LLM with formatted prompt
|
|
772
|
+
prompt = REFINER_PROMPT_TEMPLATE.format(
|
|
773
|
+
refiner_prompt=refiner_prompt,
|
|
774
|
+
candidate_to_improve=json.dumps(current_params, indent=2),
|
|
775
|
+
evaluation_feedback=current_feedback,
|
|
776
|
+
)
|
|
777
|
+
raw_output = refiner_lm(prompt).strip()
|
|
778
|
+
# Strip markdown code fences if present
|
|
779
|
+
if raw_output.startswith("```"):
|
|
780
|
+
# Remove first line (```json or ```) and last line (```)
|
|
781
|
+
lines = raw_output.split("\n")
|
|
782
|
+
raw_output = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]).strip()
|
|
783
|
+
|
|
784
|
+
try:
|
|
785
|
+
parsed_refined = json.loads(raw_output)
|
|
786
|
+
if not isinstance(parsed_refined, dict):
|
|
787
|
+
raise ValueError(f"Expected JSON dict, got {type(parsed_refined).__name__}")
|
|
788
|
+
except (json.JSONDecodeError, ValueError) as parse_err:
|
|
789
|
+
# JSON parse failed: record error so refiner can learn, continue to next iteration
|
|
790
|
+
all_attempts.append(
|
|
791
|
+
{
|
|
792
|
+
"iteration": refinement_iter + 1,
|
|
793
|
+
"error": f"JSON parse error: {parse_err}",
|
|
794
|
+
"raw_output": raw_output[:2000],
|
|
795
|
+
"score": -1e9,
|
|
796
|
+
}
|
|
797
|
+
)
|
|
798
|
+
continue
|
|
799
|
+
|
|
800
|
+
# Reconstruct full candidate: refined params + original refiner_prompt
|
|
801
|
+
refined_candidate_dict = {**parsed_refined, "refiner_prompt": candidate.get("refiner_prompt", "")}
|
|
802
|
+
refined_score, refined_output, refined_eval_side_info = self._call_evaluator(
|
|
803
|
+
refined_candidate_dict, example
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Update best evals with this refinement evaluation
|
|
807
|
+
self._update_best_example_evals(example, refined_score, refined_eval_side_info)
|
|
808
|
+
|
|
809
|
+
# Track attempt
|
|
810
|
+
all_attempts.append(
|
|
811
|
+
{
|
|
812
|
+
"iteration": refinement_iter + 1,
|
|
813
|
+
"candidate": parsed_refined,
|
|
814
|
+
"score": refined_score,
|
|
815
|
+
"side_info": refined_eval_side_info,
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Update best if improved
|
|
820
|
+
if refined_score > best_score:
|
|
821
|
+
best_score = refined_score
|
|
822
|
+
best_candidate = refined_candidate_dict # Track the actual candidate dict
|
|
823
|
+
best_side_info = refined_eval_side_info
|
|
824
|
+
current_params = parsed_refined
|
|
825
|
+
else:
|
|
826
|
+
# Stop when no improvement
|
|
827
|
+
break
|
|
828
|
+
|
|
829
|
+
except Exception as e:
|
|
830
|
+
# Refinement failed, record error and stop
|
|
831
|
+
all_attempts.append(
|
|
832
|
+
{
|
|
833
|
+
"iteration": refinement_iter + 1,
|
|
834
|
+
"error": str(e),
|
|
835
|
+
"score": -1e9,
|
|
836
|
+
}
|
|
837
|
+
)
|
|
838
|
+
break
|
|
839
|
+
|
|
840
|
+
return best_score, best_candidate, best_side_info, all_attempts
|
|
841
|
+
|
|
842
|
+
def _format_all_attempts_feedback(self, all_attempts: list[dict]) -> str:
|
|
843
|
+
"""Format all attempts (including original) into readable feedback for the refiner LLM.
|
|
844
|
+
|
|
845
|
+
This provides the refiner with full history of optimization attempts,
|
|
846
|
+
allowing it to understand the progression and avoid repeating failed approaches.
|
|
847
|
+
"""
|
|
848
|
+
return json.dumps(all_attempts, indent=2, default=str)
|
|
849
|
+
|
|
850
|
+
def make_reflective_dataset(
|
|
851
|
+
self,
|
|
852
|
+
candidate: dict[str, str],
|
|
853
|
+
eval_batch: EvaluationBatch,
|
|
854
|
+
components_to_update: list[str],
|
|
855
|
+
) -> Mapping[str, Sequence[Mapping[str, Any]]]:
|
|
856
|
+
"""Format evaluation results into per-component reflection datasets.
|
|
857
|
+
|
|
858
|
+
For each component being updated, produces a list of dicts (one per
|
|
859
|
+
example) combining shared SideInfo fields with any
|
|
860
|
+
``<component>_specific_info`` data. The ``"scores"`` key is renamed
|
|
861
|
+
to ``"Scores (Higher is Better)"`` for clarity in the LLM prompt.
|
|
862
|
+
"""
|
|
863
|
+
scores, side_infos = eval_batch.scores, eval_batch.trajectories
|
|
864
|
+
assert side_infos is not None
|
|
865
|
+
ret: dict[str, list[dict[str, Any]]] = {}
|
|
866
|
+
for component_name in components_to_update:
|
|
867
|
+
ret[component_name] = []
|
|
868
|
+
for score, side_info in zip(scores, side_infos, strict=False):
|
|
869
|
+
ret[component_name].append({})
|
|
870
|
+
for k, v in side_info.items():
|
|
871
|
+
if k == "scores":
|
|
872
|
+
ret[component_name][-1]["Scores (Higher is Better)"] = v
|
|
873
|
+
elif not k.endswith("_specific_info"):
|
|
874
|
+
ret[component_name][-1][k] = v
|
|
875
|
+
elif k == f"{component_name}_specific_info":
|
|
876
|
+
ret[component_name][-1].update(v)
|
|
877
|
+
else:
|
|
878
|
+
continue
|
|
879
|
+
return ret
|