cmpnd 0.77.0__tar.gz → 0.78.0__tar.gz
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.
- {cmpnd-0.77.0 → cmpnd-0.78.0}/PKG-INFO +1 -1
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_gepa_patch.py +45 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/callback.py +38 -5
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/context.py +24 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/gepa.py +4 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/sdk-instrumentation.md +36 -1
- {cmpnd-0.77.0 → cmpnd-0.78.0}/pyproject.toml +1 -1
- cmpnd-0.78.0/tests/test_optimization_phase_callback.py +196 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_run_attribution_paths.py +101 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/uv.lock +1 -1
- {cmpnd-0.77.0 → cmpnd-0.78.0}/.gitignore +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/AGENTS.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/CLAUDE.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/authored.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/views.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/named_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/program_naming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/cli.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/client.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/check-pip-install.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/install-test-env.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/release-gate.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/release-real-models.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/selfhost_compat.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_credential_route.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_traced_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/continuation_task.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_pandas.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_authored_metric.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_continuation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_custom_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/drain.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_provider.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_reliability.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_result.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/mint_bedrock_token.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_bedrock_converse.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_deploy_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_multi_client_stress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_client_committee.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_continuation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_loop.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_stress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_provider_roles.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_real_provider_twins.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_reliability.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_reliability_classify.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_rlm_wasm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_trace_search.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_binding_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces_program_and_module.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client_floor.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_context.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_kwargs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_selection.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_values.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_decision_client_spans.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy_program_id.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployment_runs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_program_id.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_execute.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_integration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm15_native_calls.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_models.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_multi_client_stress_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_no_signature_hash_on_the_wire.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_program_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_id_deprecation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_identity.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_names_in_deploy.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_naming.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_programs_create.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_release_gate_script.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_release_real_models_script.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_replaced_names.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_retry.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_seal_lm_credential.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_signature_inputs.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_span_references.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_system_one.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_context_concurrency.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_upload_auth_headers.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_xxh64.py +0 -0
|
@@ -9,6 +9,10 @@ module-style (``from . import identity``); basedpyright can't resolve
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import contextvars
|
|
12
|
+
import functools
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from . import context
|
|
12
16
|
|
|
13
17
|
_gepa_patched = False
|
|
14
18
|
|
|
@@ -39,6 +43,43 @@ def _gepa_supports_callbacks() -> bool:
|
|
|
39
43
|
return False
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
# Marks a metric wrapper _judge_metric_calls installed, so a compile nested in another
|
|
47
|
+
# (TrackedGEPA under auto_instrument) leaves the outer wrapper in place. An attribute
|
|
48
|
+
# on our own wrapper, read with getattr, because the user's metric may be any
|
|
49
|
+
# callable: a dataclass instance is unhashable, so no set or dict can hold it.
|
|
50
|
+
_JUDGED_METRIC_MARK = "_cmpnd_judges_metric_calls"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _judge_metric_calls(gepa) -> Callable[[], None]:
|
|
54
|
+
"""Run each of ``gepa``'s metric calls as the metric phase; return the undo.
|
|
55
|
+
|
|
56
|
+
dspy.GEPA reads its metric from ``metric_fn`` once its compile starts, so
|
|
57
|
+
swapping in a wrapper for the compile's span reaches every metric call, those
|
|
58
|
+
DSPy Evaluate makes and those GEPA's feedback makes. A trace a grading module
|
|
59
|
+
roots inside a call is then tagged ``optimization_phase: metric``, as a hosted
|
|
60
|
+
run's judge traces are, while the rollout the metric grades ran before it and
|
|
61
|
+
carries no phase. The wrapper keeps the metric's signature (``functools.wraps``)
|
|
62
|
+
for DSPy's checks of it, and the undo restores the optimizer's own metric.
|
|
63
|
+
"""
|
|
64
|
+
metric = getattr(gepa, "metric_fn", None)
|
|
65
|
+
if not callable(metric) or getattr(metric, _JUDGED_METRIC_MARK, False) is True:
|
|
66
|
+
return lambda: None
|
|
67
|
+
|
|
68
|
+
@functools.wraps(metric)
|
|
69
|
+
def judged_metric(*args, **kwargs):
|
|
70
|
+
with context.optimization_phase("metric"):
|
|
71
|
+
return metric(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
setattr(judged_metric, _JUDGED_METRIC_MARK, True)
|
|
74
|
+
gepa.metric_fn = judged_metric
|
|
75
|
+
|
|
76
|
+
def restore() -> None:
|
|
77
|
+
if getattr(gepa, "metric_fn", None) is judged_metric:
|
|
78
|
+
gepa.metric_fn = metric
|
|
79
|
+
|
|
80
|
+
return restore
|
|
81
|
+
|
|
82
|
+
|
|
42
83
|
def _run_tracked_compile(
|
|
43
84
|
gepa,
|
|
44
85
|
student,
|
|
@@ -137,8 +178,10 @@ def _run_tracked_compile(
|
|
|
137
178
|
trk.generated_program_name = program_naming.generated_program_name(student)
|
|
138
179
|
|
|
139
180
|
restore_attribution = callback.start_trace_attribution()
|
|
181
|
+
restore_metric = None
|
|
140
182
|
|
|
141
183
|
try:
|
|
184
|
+
restore_metric = _judge_metric_calls(gepa)
|
|
142
185
|
result = run_original()
|
|
143
186
|
|
|
144
187
|
if not has_callbacks:
|
|
@@ -164,6 +207,8 @@ def _run_tracked_compile(
|
|
|
164
207
|
raise
|
|
165
208
|
|
|
166
209
|
finally:
|
|
210
|
+
if restore_metric is not None:
|
|
211
|
+
restore_metric()
|
|
167
212
|
restore_attribution()
|
|
168
213
|
|
|
169
214
|
|
|
@@ -517,6 +517,11 @@ def _finish_reason(response: Any) -> str | None:
|
|
|
517
517
|
return "stop"
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
# The `optimization.role` an LM call in each GEPA phase is priced under. A metric scores a
|
|
521
|
+
# rollout, so its calls are rollout spend; reflection proposes the next candidate.
|
|
522
|
+
_OPTIMIZATION_PHASE_ROLES = {"metric": "rollout", "reflection": "reflection"}
|
|
523
|
+
|
|
524
|
+
|
|
520
525
|
class _CallbackBase:
|
|
521
526
|
"""Shared state and span-tracking primitives for the CmpndCallback mixins."""
|
|
522
527
|
|
|
@@ -532,6 +537,7 @@ class _CallbackBase:
|
|
|
532
537
|
max_output_field_size: int | None = None,
|
|
533
538
|
stream_trace_starts: bool | None = None,
|
|
534
539
|
own_config: configuration.Config | None = None,
|
|
540
|
+
optimization_phase: str | None = None,
|
|
535
541
|
) -> None:
|
|
536
542
|
# A client passes its own config here; the auto_instrument callback leaves
|
|
537
543
|
# it None and reads the process-global config live, so a reconfigure()
|
|
@@ -541,6 +547,19 @@ class _CallbackBase:
|
|
|
541
547
|
# later cmpnd.configure(). That fixedness is what keeps a client
|
|
542
548
|
# orthogonal to the global config.
|
|
543
549
|
self._own_config = own_config
|
|
550
|
+
# The optimization phase this callback traces for a run it does not own: an
|
|
551
|
+
# executor that scores a hosted rollout with the author's metric ("metric") or runs
|
|
552
|
+
# the author's GEPA proposer ("reflection"). Such a process knows the program the
|
|
553
|
+
# run optimizes and not the run's id, which the ingest key it pushes with decides.
|
|
554
|
+
# With a phase set, an LM call outside any trace becomes a standalone GEPA trace as
|
|
555
|
+
# a local optimization's judge and reflection calls do, and a trace a module opens
|
|
556
|
+
# is tagged with the phase. None is every other caller.
|
|
557
|
+
if optimization_phase not in (None, *_OPTIMIZATION_PHASE_ROLES):
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"optimization_phase must be one of {sorted(_OPTIMIZATION_PHASE_ROLES)} or None, "
|
|
560
|
+
f"got {optimization_phase!r}"
|
|
561
|
+
)
|
|
562
|
+
self._optimization_phase = optimization_phase
|
|
544
563
|
# Set once this callback has warned that a declared name replaced the program
|
|
545
564
|
# id its config read from the environment, so the warning appears once per client.
|
|
546
565
|
self._warned_name_over_id = False
|
|
@@ -1348,6 +1367,12 @@ class _ModuleMixin(_CallbackBase):
|
|
|
1348
1367
|
# Apply default tags
|
|
1349
1368
|
if config and config.default_tags:
|
|
1350
1369
|
trace.tags.update(config.default_tags)
|
|
1370
|
+
# A trace opened while this callback traces a phase, or while a local run
|
|
1371
|
+
# calls its metric, is the metric's or the proposer's, never a rollout, even
|
|
1372
|
+
# when its module is the program's own class.
|
|
1373
|
+
phase = self._optimization_phase or context.get_current_optimization_phase()
|
|
1374
|
+
if phase is not None:
|
|
1375
|
+
trace.tags["optimization_phase"] = phase
|
|
1351
1376
|
|
|
1352
1377
|
return trace
|
|
1353
1378
|
|
|
@@ -1401,7 +1426,9 @@ class _LMMixin(_CallbackBase):
|
|
|
1401
1426
|
span.model_name = getattr(instance, "model", None)
|
|
1402
1427
|
span.model_provider = helpers.infer_provider(instance)
|
|
1403
1428
|
|
|
1404
|
-
if self.
|
|
1429
|
+
if self._optimization_phase is not None:
|
|
1430
|
+
span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[self._optimization_phase]
|
|
1431
|
+
elif self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None:
|
|
1405
1432
|
span.attributes["optimization.role"] = "rollout"
|
|
1406
1433
|
|
|
1407
1434
|
# The usage fields read one named value each out of the raw kwargs. The
|
|
@@ -1603,7 +1630,9 @@ class _LMMixin(_CallbackBase):
|
|
|
1603
1630
|
# must NOT also be added to _orphaned_lm_usage / untraced_lm_usage — that
|
|
1604
1631
|
# would double-count them in the optimization total.
|
|
1605
1632
|
current_trace = context.get_current_trace()
|
|
1606
|
-
if current_trace is None and
|
|
1633
|
+
if current_trace is None and (
|
|
1634
|
+
self._active_optimization_run_id is not None or self._optimization_phase is not None
|
|
1635
|
+
):
|
|
1607
1636
|
self._emit_optimization_lm_trace(span)
|
|
1608
1637
|
return # handled as a standalone GEPA-process trace
|
|
1609
1638
|
|
|
@@ -1644,14 +1673,18 @@ class _LMMixin(_CallbackBase):
|
|
|
1644
1673
|
carries the distinction. Each becomes a standalone one-span
|
|
1645
1674
|
trace so the prompt, response, tokens and cost are visible alongside the
|
|
1646
1675
|
student traces instead of being reduced to an aggregate token count.
|
|
1676
|
+
|
|
1677
|
+
A callback built with an ``optimization_phase`` takes its phase from that
|
|
1678
|
+
instead, and may have no run id: it traces the metric or the proposer of a
|
|
1679
|
+
run whose id the ingest key the trace is pushed with decides.
|
|
1647
1680
|
"""
|
|
1648
1681
|
run_id = self._active_optimization_run_id
|
|
1649
|
-
if not run_id:
|
|
1682
|
+
if not run_id and self._optimization_phase is None:
|
|
1650
1683
|
return
|
|
1651
1684
|
|
|
1652
|
-
phase = "metric" if self._active_eval_run is not None else "reflection"
|
|
1685
|
+
phase = self._optimization_phase or ("metric" if self._active_eval_run is not None else "reflection")
|
|
1653
1686
|
span.attributes["gepa.phase"] = phase
|
|
1654
|
-
span.attributes["optimization.role"] =
|
|
1687
|
+
span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[phase]
|
|
1655
1688
|
|
|
1656
1689
|
trace = models.TraceBuilder(
|
|
1657
1690
|
trace_id=span.trace_id or uuid4(),
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from contextlib import contextmanager
|
|
5
7
|
from contextvars import ContextVar
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
from typing import TYPE_CHECKING, Any
|
|
@@ -27,6 +29,12 @@ _current_eval_run: ContextVar[models.EvalRunBuilder | None] = ContextVar("cmpnd_
|
|
|
27
29
|
# evals). Prefixed text id (`op_…`), minted via cmpnd.identity.
|
|
28
30
|
_current_optimization_run_id: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_run_id", default=None)
|
|
29
31
|
|
|
32
|
+
# The optimization phase the code running now serves, when that code is an
|
|
33
|
+
# optimizer's metric rather than the program it optimizes. A local GEPA run sets it
|
|
34
|
+
# around each metric call, so a trace the metric's grading module roots is tagged as
|
|
35
|
+
# the metric's, as a hosted run's kernel tags it through its callback's own phase.
|
|
36
|
+
_current_optimization_phase: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_phase", default=None)
|
|
37
|
+
|
|
30
38
|
|
|
31
39
|
def get_current_trace() -> models.TraceBuilder | None:
|
|
32
40
|
"""Get the current trace."""
|
|
@@ -79,6 +87,21 @@ def set_current_optimization_run_id(optimization_run_id: str | None) -> None:
|
|
|
79
87
|
_current_optimization_run_id.set(optimization_run_id)
|
|
80
88
|
|
|
81
89
|
|
|
90
|
+
def get_current_optimization_phase() -> str | None:
|
|
91
|
+
"""The optimization phase the running code serves, or None outside a metric call."""
|
|
92
|
+
return _current_optimization_phase.get()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@contextmanager
|
|
96
|
+
def optimization_phase(phase: str) -> Generator[None]:
|
|
97
|
+
"""Run the block as the optimization phase ``phase``, restoring the outer phase after."""
|
|
98
|
+
token = _current_optimization_phase.set(phase)
|
|
99
|
+
try:
|
|
100
|
+
yield
|
|
101
|
+
finally:
|
|
102
|
+
_current_optimization_phase.reset(token)
|
|
103
|
+
|
|
104
|
+
|
|
82
105
|
# The program each optimization run this process started optimizes, by run id. A trace
|
|
83
106
|
# linked to a run that names no program of its own takes its run's, so everything a run
|
|
84
107
|
# emits is reachable through that program: GEPA's reflection and judge calls, a judge
|
|
@@ -148,4 +171,5 @@ def clear_context() -> None:
|
|
|
148
171
|
_span_stack.set(())
|
|
149
172
|
_current_eval_run.set(None)
|
|
150
173
|
_current_optimization_run_id.set(None)
|
|
174
|
+
_current_optimization_phase.set(None)
|
|
151
175
|
_optimization_program_ids.clear()
|
|
@@ -205,9 +205,11 @@ class TrackedGEPA:
|
|
|
205
205
|
existing_callbacks = self._gepa.gepa_kwargs.get("callbacks") or []
|
|
206
206
|
self._gepa.gepa_kwargs["callbacks"] = existing_callbacks + [gcb]
|
|
207
207
|
restore_attribution = None
|
|
208
|
+
restore_metric = None
|
|
208
209
|
|
|
209
210
|
try:
|
|
210
211
|
restore_attribution = gcb.start_trace_attribution()
|
|
212
|
+
restore_metric = _gepa_patch._judge_metric_calls(self._gepa)
|
|
211
213
|
# Run optimization (trainset/valset are keyword-only in GEPA)
|
|
212
214
|
compile_token = _gepa_patch._tracked_compile_optimizer.set(self._gepa)
|
|
213
215
|
try:
|
|
@@ -246,6 +248,8 @@ class TrackedGEPA:
|
|
|
246
248
|
raise
|
|
247
249
|
|
|
248
250
|
finally:
|
|
251
|
+
if restore_metric is not None:
|
|
252
|
+
restore_metric()
|
|
249
253
|
if restore_attribution is not None:
|
|
250
254
|
restore_attribution()
|
|
251
255
|
# Remove our callback from gepa_kwargs
|
|
@@ -180,6 +180,7 @@ Everything lives in `python-sdk/cmpnd/context.py`:
|
|
|
180
180
|
- `_span_stack` — an immutable tuple of `SpanBuilder`s. `push_span` and `pop_span` set a new tuple rather than mutating the old one, because a context copy shares the objects its parent context holds: a mutable stack would be one list shared by every sibling copy, so concurrent runs (GEPA's `DspyAdapter.batch_evaluate`, which runs each candidate in a `copy_context()`, or `asyncio.gather` over `acall`) would read each other's spans as their parent. `get_current_span` returns the innermost. `tests/test_trace_context_concurrency.py` pins this.
|
|
181
181
|
- `_current_eval_run` (`context.py:24`) — set while an `EvalHandler` is active.
|
|
182
182
|
- `_current_optimization_run_id` (`context.py:29`) — set while a patched `GEPA.compile` is running, so evals triggered inside it can be linked back.
|
|
183
|
+
- `_current_optimization_phase` — set to `"metric"` for the span of each metric call a tracked GEPA compile makes, so a trace the metric's grading module roots is tagged as the metric's. `context.optimization_phase(phase)` sets it for a block.
|
|
183
184
|
|
|
184
185
|
`create_child_span` at `context.py:83` is the standard constructor: it reads the current trace + span, assigns `trace_id` and `parent_span_id`, and returns an unpushed `SpanBuilder`. The callback uses it for every child span; so does the `start_span` context manager below. Pass `root=True` to have it ignore the stack and leave `parent_span_id` unset, for a span that starts a trace beneath one that does not parent it.
|
|
185
186
|
|
|
@@ -202,7 +203,41 @@ callback never fires. This prevents an ordinary evaluation after a failure from
|
|
|
202
203
|
being queued under the failed run or being claimed as the next optimization's
|
|
203
204
|
seed evidence.
|
|
204
205
|
|
|
205
|
-
|
|
206
|
+
### A callback that traces one phase of a run it does not own
|
|
207
|
+
|
|
208
|
+
`CmpndCallback(optimization_phase=...)` takes `"metric"` or `"reflection"`. A worker
|
|
209
|
+
kernel builds one around a hosted run's metric and around its custom GEPA proposer.
|
|
210
|
+
That process knows the program the run optimizes, from its config, and not the run's
|
|
211
|
+
id, which serve stamps from the ingest key the traces are pushed with. Such a callback
|
|
212
|
+
makes those calls look like a local optimization's:
|
|
213
|
+
|
|
214
|
+
- An LM call outside any trace becomes the standalone one-span `GEPA` trace that
|
|
215
|
+
`_emit_optimization_lm_trace` writes for a local run. Its span carries `gepa.phase`
|
|
216
|
+
and `optimization.role`, and its trace carries the `optimization_phase` tag. The
|
|
217
|
+
callback takes the phase from its constructor, not from an active eval run, and
|
|
218
|
+
leaves `optimization_run_id` unset.
|
|
219
|
+
- A trace that a dspy module opens is tagged `optimization_phase`, so a program's trace
|
|
220
|
+
list tells a judge module from a rollout of the same class.
|
|
221
|
+
- Every LM span gets `optimization.role`: `rollout` for a metric, `reflection` for a
|
|
222
|
+
proposer.
|
|
223
|
+
|
|
224
|
+
A local run owns its run, so its callback declares no phase. It tags its judge traces
|
|
225
|
+
through context instead. While a tracked GEPA compile runs, through `auto_instrument`,
|
|
226
|
+
`TrackedGEPA` or a client's `optimize_program`, `_gepa_patch._judge_metric_calls`
|
|
227
|
+
swaps the optimizer's `metric_fn` for a wrapper that runs each call under
|
|
228
|
+
`context.optimization_phase("metric")`, and restores the metric when the compile ends.
|
|
229
|
+
`_create_trace` reads that phase when its callback has none. So a judge module the
|
|
230
|
+
metric grades through roots a trace tagged `optimization_phase: metric`, as a hosted
|
|
231
|
+
judge's module trace is. The rollout the metric grades ran before the call and carries
|
|
232
|
+
no tag. The context phase changes only that tag: a direct judge or reflection call is
|
|
233
|
+
still the trace `_emit_optimization_lm_trace` builds, and module spans get no
|
|
234
|
+
`optimization.role`.
|
|
235
|
+
|
|
236
|
+
`tests/test_optimization_phase_callback.py` drives both paths, and the context phase,
|
|
237
|
+
through real DSPy dispatch. `tests/test_run_attribution_paths.py` runs a real GEPA
|
|
238
|
+
search whose metric grades through a module.
|
|
239
|
+
|
|
240
|
+
For tests that need a clean slate, `clear_context()` in `context.py` resets these vars — see the `cmpnd-testing` skill and [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks".
|
|
206
241
|
|
|
207
242
|
### Across a real fork
|
|
208
243
|
|
|
@@ -5,7 +5,7 @@ name = "cmpnd"
|
|
|
5
5
|
# it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
|
|
6
6
|
# hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
|
|
7
7
|
# this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.78.0"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""A callback that traces one optimization phase on behalf of a run it does not own.
|
|
2
|
+
|
|
3
|
+
A worker kernel scores a hosted rollout with the author's metric and runs the author's
|
|
4
|
+
GEPA proposer, and both may call a model. The kernel knows the program the run
|
|
5
|
+
optimizes but not the run's id: serve stamps that from the run key the traces are
|
|
6
|
+
pushed with. So the kernel builds its callback with ``optimization_phase`` and these
|
|
7
|
+
calls must come out as a local optimization's judge and reflection calls do:
|
|
8
|
+
|
|
9
|
+
* an LM call outside any trace is a standalone one-span ``GEPA`` trace, with
|
|
10
|
+
``gepa.phase`` and ``optimization.role`` on its span and ``optimization_phase`` in
|
|
11
|
+
its tags;
|
|
12
|
+
* a call made through a dspy module roots a trace at that module, tagged with the phase
|
|
13
|
+
so a program's trace list tells it from a rollout of the same module class;
|
|
14
|
+
* every such trace carries the configured program id, and no run id.
|
|
15
|
+
|
|
16
|
+
A local GEPA run owns its run, so its callback declares no phase. It runs each metric
|
|
17
|
+
call under ``context.optimization_phase("metric")`` instead, and a trace the metric's
|
|
18
|
+
grading module roots there carries the same tag a hosted judge trace does.
|
|
19
|
+
|
|
20
|
+
Driven through real DSPy dispatch (``dspy.Predict`` and ``DummyLM``), because the
|
|
21
|
+
paths under test are which callback hooks DSPy fires and with what trace in scope.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import dataclasses
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import dspy
|
|
30
|
+
import pytest
|
|
31
|
+
from dspy.utils.dummies import DummyLM
|
|
32
|
+
|
|
33
|
+
from cmpnd import configuration, context
|
|
34
|
+
from cmpnd.configuration import configure
|
|
35
|
+
from cmpnd.context import clear_context
|
|
36
|
+
from tests._traced_run import make_callback, run
|
|
37
|
+
|
|
38
|
+
PROGRAM_ID = "p_0123456789Ab"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture(autouse=True)
|
|
42
|
+
def configured_program():
|
|
43
|
+
"""The kernel's config: the SDK configured, holding the deployment's program id."""
|
|
44
|
+
config = configure(api_key="test_key", endpoint="http://localhost:8000")
|
|
45
|
+
with configuration._config_lock:
|
|
46
|
+
configuration._config = dataclasses.replace(config, program_id=PROGRAM_ID)
|
|
47
|
+
yield
|
|
48
|
+
clear_context()
|
|
49
|
+
configuration._config = None
|
|
50
|
+
from cmpnd.exporter import shutdown_exporter
|
|
51
|
+
|
|
52
|
+
shutdown_exporter()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lm(answers: list[dict[str, str]]) -> DummyLM:
|
|
56
|
+
return DummyLM(answers, adapter=dspy.ChatAdapter())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _direct_judge(lm: Any):
|
|
60
|
+
"""A metric that asks its model directly, with no dspy module in between."""
|
|
61
|
+
|
|
62
|
+
def judge() -> Any:
|
|
63
|
+
return lm("Is 'four' right for 2+2? Say yes or no.")
|
|
64
|
+
|
|
65
|
+
return judge
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestADirectCallIsAStandaloneTrace:
|
|
69
|
+
@pytest.mark.parametrize(("phase", "role"), [("metric", "rollout"), ("reflection", "reflection")])
|
|
70
|
+
def test_it_is_one_gepa_span_carrying_the_phase_and_the_program(self, phase: str, role: str):
|
|
71
|
+
lm = _lm([{"answer": "yes"}])
|
|
72
|
+
_, exp = run(_direct_judge(lm), lm, dspy.ChatAdapter(), callback=make_callback(optimization_phase=phase))
|
|
73
|
+
|
|
74
|
+
assert len(exp.traces) == 1, exp.traces
|
|
75
|
+
trace = exp.traces[0]
|
|
76
|
+
assert trace.module_class == "GEPA"
|
|
77
|
+
assert trace.tags.get("optimization_phase") == phase
|
|
78
|
+
assert trace.program_id == PROGRAM_ID
|
|
79
|
+
# Serve stamps the run from the key the trace is pushed with; the kernel claims none.
|
|
80
|
+
assert trace.optimization_run_id is None
|
|
81
|
+
assert len(trace._batch_spans) == 1
|
|
82
|
+
span = trace._batch_spans[0]
|
|
83
|
+
assert span.span_type.value == "lm_call"
|
|
84
|
+
assert span.parent_span_id is None
|
|
85
|
+
assert span.trace_id == trace.trace_id
|
|
86
|
+
wire = span.to_api_dict()
|
|
87
|
+
assert wire["attributes"]["gepa.phase"] == phase
|
|
88
|
+
assert wire["attributes"]["optimization.role"] == role
|
|
89
|
+
|
|
90
|
+
def test_each_call_is_its_own_trace(self):
|
|
91
|
+
lm = _lm([{"answer": "yes"}, {"answer": "no"}])
|
|
92
|
+
judge = _direct_judge(lm)
|
|
93
|
+
|
|
94
|
+
def judge_twice() -> None:
|
|
95
|
+
judge()
|
|
96
|
+
judge()
|
|
97
|
+
|
|
98
|
+
_, exp = run(judge_twice, lm, dspy.ChatAdapter(), callback=make_callback(optimization_phase="metric"))
|
|
99
|
+
|
|
100
|
+
assert len(exp.traces) == 2
|
|
101
|
+
assert len({trace.trace_id for trace in exp.traces}) == 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestACallThroughAModuleRootsItsOwnTrace:
|
|
105
|
+
@pytest.mark.parametrize(("phase", "role"), [("metric", "rollout"), ("reflection", "reflection")])
|
|
106
|
+
def test_the_module_trace_is_tagged_with_the_phase(self, phase: str, role: str):
|
|
107
|
+
lm = _lm([{"verdict": "right"}])
|
|
108
|
+
grader = dspy.Predict("question, answer -> verdict: str")
|
|
109
|
+
_, exp = run(
|
|
110
|
+
grader,
|
|
111
|
+
lm,
|
|
112
|
+
dspy.ChatAdapter(),
|
|
113
|
+
callback=make_callback(optimization_phase=phase),
|
|
114
|
+
question="2+2?",
|
|
115
|
+
answer="four",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
assert len(exp.traces) == 1, exp.traces
|
|
119
|
+
trace = exp.traces[0]
|
|
120
|
+
assert trace.module_class == "Predict"
|
|
121
|
+
assert trace.tags.get("optimization_phase") == phase
|
|
122
|
+
assert trace.program_id == PROGRAM_ID
|
|
123
|
+
assert trace.optimization_run_id is None
|
|
124
|
+
lm_spans = [span.to_api_dict() for span in exp.spans if span.span_type.value == "lm_call"]
|
|
125
|
+
assert lm_spans, exp.wire()
|
|
126
|
+
assert all(span["attributes"]["optimization.role"] == role for span in lm_spans)
|
|
127
|
+
assert all("gepa.phase" not in span["attributes"] for span in lm_spans)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestWithoutAPhase:
|
|
131
|
+
def test_a_program_trace_carries_no_phase(self):
|
|
132
|
+
"""A rollout's own callback declares no phase, so its traces stay rollouts."""
|
|
133
|
+
lm = _lm([{"answer": "4"}])
|
|
134
|
+
_, exp = run(dspy.Predict("question -> answer"), lm, dspy.ChatAdapter(), question="2+2?")
|
|
135
|
+
|
|
136
|
+
assert len(exp.traces) == 1
|
|
137
|
+
assert "optimization_phase" not in exp.traces[0].tags
|
|
138
|
+
assert all("optimization.role" not in span.to_api_dict()["attributes"] for span in exp.spans)
|
|
139
|
+
|
|
140
|
+
def test_a_direct_call_outside_an_optimization_is_no_trace(self):
|
|
141
|
+
lm = _lm([{"answer": "yes"}])
|
|
142
|
+
_, exp = run(_direct_judge(lm), lm, dspy.ChatAdapter())
|
|
143
|
+
|
|
144
|
+
assert exp.traces == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestALocalMetricCallsPhase:
|
|
148
|
+
"""The phase a local run sets around its metric, read by a callback that declares none."""
|
|
149
|
+
|
|
150
|
+
def test_a_module_the_metric_grades_through_is_tagged_as_the_metric(self):
|
|
151
|
+
lm = _lm([{"verdict": "right"}])
|
|
152
|
+
grader = dspy.Predict("question, answer -> verdict: str")
|
|
153
|
+
|
|
154
|
+
def grade() -> Any:
|
|
155
|
+
with context.optimization_phase("metric"):
|
|
156
|
+
return grader(question="2+2?", answer="four")
|
|
157
|
+
|
|
158
|
+
_, exp = run(grade, lm, dspy.ChatAdapter())
|
|
159
|
+
|
|
160
|
+
assert len(exp.traces) == 1, exp.traces
|
|
161
|
+
assert exp.traces[0].module_class == "Predict"
|
|
162
|
+
assert exp.traces[0].tags.get("optimization_phase") == "metric"
|
|
163
|
+
# Only the trace's tag changes: its spans are built as any module trace's are.
|
|
164
|
+
assert all("optimization.role" not in span.to_api_dict()["attributes"] for span in exp.spans)
|
|
165
|
+
|
|
166
|
+
def test_a_program_traced_after_the_metric_returns_carries_no_phase(self):
|
|
167
|
+
lm = _lm([{"verdict": "right"}, {"answer": "4"}])
|
|
168
|
+
grader = dspy.Predict("question, answer -> verdict: str")
|
|
169
|
+
program = dspy.Predict("question -> answer")
|
|
170
|
+
|
|
171
|
+
def grade_then_run() -> None:
|
|
172
|
+
with context.optimization_phase("metric"):
|
|
173
|
+
grader(question="2+2?", answer="four")
|
|
174
|
+
program(question="2+2?")
|
|
175
|
+
|
|
176
|
+
_, exp = run(grade_then_run, lm, dspy.ChatAdapter())
|
|
177
|
+
|
|
178
|
+
assert [trace.tags.get("optimization_phase") for trace in exp.traces] == ["metric", None]
|
|
179
|
+
|
|
180
|
+
def test_a_direct_call_outside_an_optimization_stays_untraced(self):
|
|
181
|
+
"""The phase tags module traces only: a direct call still needs a run to trace."""
|
|
182
|
+
lm = _lm([{"answer": "yes"}])
|
|
183
|
+
judge = _direct_judge(lm)
|
|
184
|
+
|
|
185
|
+
def judge_in_phase() -> None:
|
|
186
|
+
with context.optimization_phase("metric"):
|
|
187
|
+
judge()
|
|
188
|
+
|
|
189
|
+
_, exp = run(judge_in_phase, lm, dspy.ChatAdapter())
|
|
190
|
+
|
|
191
|
+
assert exp.traces == []
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_an_unknown_phase_is_refused():
|
|
195
|
+
with pytest.raises(ValueError, match="optimization_phase"):
|
|
196
|
+
make_callback(optimization_phase="rollout")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import dataclasses
|
|
6
7
|
from types import SimpleNamespace
|
|
7
8
|
from unittest import mock
|
|
8
9
|
|
|
@@ -222,6 +223,106 @@ def test_real_gepa_seed_evaluation_starts_with_run_id(captured, native_gepa, wra
|
|
|
222
223
|
captured.exporter.export_optimization_start.assert_called_once()
|
|
223
224
|
|
|
224
225
|
|
|
226
|
+
class Grader(dspy.Module):
|
|
227
|
+
"""A judge the metric grades through: a dspy module of its own, on its own model."""
|
|
228
|
+
|
|
229
|
+
def __init__(self):
|
|
230
|
+
super().__init__()
|
|
231
|
+
self.judge = dspy.Predict("question, answer -> verdict: str")
|
|
232
|
+
self.judge.set_lm(DummyLM([{"verdict": "right"}] * 8))
|
|
233
|
+
|
|
234
|
+
def forward(self, question: str, answer: str):
|
|
235
|
+
return self.judge(question=question, answer=answer)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.parametrize("wrapper", ["automatic", "tracked"])
|
|
239
|
+
def test_real_gepa_tags_the_metrics_judge_trace_and_not_the_rollout(captured, native_gepa, wrapper):
|
|
240
|
+
if wrapper == "tracked" and not _gepa_patch._gepa_supports_callbacks():
|
|
241
|
+
pytest.skip("native GEPA callback path requires callback support")
|
|
242
|
+
program = dspy.Predict("question -> answer")
|
|
243
|
+
examples = [dspy.Example(question="hello", answer="HELLO").with_inputs("question")]
|
|
244
|
+
grader = Grader()
|
|
245
|
+
|
|
246
|
+
def metric(example, prediction, trace=None, pred_name=None, pred_trace=None):
|
|
247
|
+
verdict = grader(question=example.question, answer=prediction.answer).verdict
|
|
248
|
+
return dspy.Prediction(score=float(verdict == "right"), feedback="graded")
|
|
249
|
+
|
|
250
|
+
kwargs = dict(metric=metric, max_metric_calls=1, reflection_lm=DummyLM([]), num_threads=1)
|
|
251
|
+
with dspy.context(callbacks=[captured.callback], lm=DummyLM([{"answer": "HELLO"}] * 8)):
|
|
252
|
+
if wrapper == "tracked":
|
|
253
|
+
with pytest.warns(DeprecationWarning):
|
|
254
|
+
optimizer = TrackedGEPA(**kwargs)
|
|
255
|
+
else:
|
|
256
|
+
cmpnd.auto_instrument()
|
|
257
|
+
optimizer = dspy.GEPA(**kwargs)
|
|
258
|
+
optimizer.compile(program, trainset=examples, valset=examples)
|
|
259
|
+
|
|
260
|
+
phases = {(row["module_class"], (row.get("tags") or {}).get("optimization_phase")) for row in captured.completed}
|
|
261
|
+
assert ("Grader", "metric") in phases, captured.completed
|
|
262
|
+
assert ("Predict", None) in phases, captured.completed
|
|
263
|
+
assert {phase for module_class, phase in phases if module_class == "Predict"} == {None}
|
|
264
|
+
# The run hands the optimizer back the metric it was built with.
|
|
265
|
+
assert optimizer_metric(optimizer) is metric
|
|
266
|
+
assert context.get_current_optimization_phase() is None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def optimizer_metric(optimizer):
|
|
270
|
+
return (optimizer._gepa if isinstance(optimizer, TrackedGEPA) else optimizer).metric_fn
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclasses.dataclass
|
|
274
|
+
class GradingMetric:
|
|
275
|
+
"""A metric object, as a user may write one: a dataclass, so unhashable."""
|
|
276
|
+
|
|
277
|
+
grader: Grader
|
|
278
|
+
|
|
279
|
+
def __call__(self, example, prediction, trace=None, pred_name=None, pred_trace=None):
|
|
280
|
+
verdict = self.grader(question=example.question, answer=prediction.answer).verdict
|
|
281
|
+
return dspy.Prediction(score=float(verdict == "right"), feedback="graded")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.mark.parametrize("wrapper", ["automatic", "tracked"])
|
|
285
|
+
def test_real_gepa_tags_the_judge_trace_of_an_unhashable_metric(captured, native_gepa, wrapper):
|
|
286
|
+
if wrapper == "tracked" and not _gepa_patch._gepa_supports_callbacks():
|
|
287
|
+
pytest.skip("native GEPA callback path requires callback support")
|
|
288
|
+
program = dspy.Predict("question -> answer")
|
|
289
|
+
examples = [dspy.Example(question="hello", answer="HELLO").with_inputs("question")]
|
|
290
|
+
metric = GradingMetric(Grader())
|
|
291
|
+
with pytest.raises(TypeError):
|
|
292
|
+
hash(metric)
|
|
293
|
+
|
|
294
|
+
kwargs = dict(metric=metric, max_metric_calls=1, reflection_lm=DummyLM([]), num_threads=1)
|
|
295
|
+
with dspy.context(callbacks=[captured.callback], lm=DummyLM([{"answer": "HELLO"}] * 8)):
|
|
296
|
+
if wrapper == "tracked":
|
|
297
|
+
with pytest.warns(DeprecationWarning):
|
|
298
|
+
optimizer = TrackedGEPA(**kwargs)
|
|
299
|
+
else:
|
|
300
|
+
cmpnd.auto_instrument()
|
|
301
|
+
optimizer = dspy.GEPA(**kwargs)
|
|
302
|
+
optimizer.compile(program, trainset=examples, valset=examples)
|
|
303
|
+
|
|
304
|
+
phases = {(row["module_class"], (row.get("tags") or {}).get("optimization_phase")) for row in captured.completed}
|
|
305
|
+
assert ("Grader", "metric") in phases, captured.completed
|
|
306
|
+
assert optimizer_metric(optimizer) is metric
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@pytest.mark.parametrize("wrapper", ["automatic_without_callbacks", "tracked"])
|
|
310
|
+
def test_a_metric_that_cannot_be_wrapped_still_ends_run_attribution(captured, monkeypatch, wrapper):
|
|
311
|
+
def refuse(_gepa):
|
|
312
|
+
raise RuntimeError("metric not wrappable")
|
|
313
|
+
|
|
314
|
+
monkeypatch.setattr(_gepa_patch, "_judge_metric_calls", refuse)
|
|
315
|
+
program = DirectProgram()
|
|
316
|
+
|
|
317
|
+
with dspy.context(callbacks=[captured.callback]):
|
|
318
|
+
with pytest.raises(RuntimeError, match="metric not wrappable"):
|
|
319
|
+
_compile_without_native_events(wrapper, program, lambda: program)
|
|
320
|
+
|
|
321
|
+
assert captured.callback._active_optimization_run_id is None
|
|
322
|
+
assert context.get_current_optimization_run_id() is None
|
|
323
|
+
assert _gepa_patch._tracked_compile_optimizer.get() is None
|
|
324
|
+
|
|
325
|
+
|
|
225
326
|
def test_tracked_gepa_under_auto_runs_instance_override_and_tracks_distinct_optimizer(captured, native_gepa):
|
|
226
327
|
if not _gepa_patch._gepa_supports_callbacks():
|
|
227
328
|
pytest.skip("native GEPA callback path requires callback support")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|