cmpnd 0.22.0__tar.gz → 0.23.1__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.22.0 → cmpnd-0.23.1}/PKG-INFO +1 -1
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/__init__.py +47 -1
- cmpnd-0.23.1/cmpnd/_exporter_pool.py +120 -0
- cmpnd-0.23.1/cmpnd/_gepa_patch.py +216 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/_program_patch.py +17 -1
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/_rlm_patch.py +15 -1
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/callback.py +66 -11
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/login.py +3 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/top.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/shell.py +2 -2
- cmpnd-0.23.1/cmpnd/client.py +183 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/eval_handler.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimizers/tracker.py +21 -15
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/packaging.py +5 -5
- cmpnd-0.23.1/docs/client.md +110 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/sdk-instrumentation.md +2 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/pyproject.toml +1 -1
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_assessments_cli.py +4 -4
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimize_review_committee.py +4 -6
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_valsets_cli.py +11 -14
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/optimization_run_probe.py +3 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/_reliability.py +35 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_optimize_client_committee.py +16 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_assessments.py +9 -14
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_auth.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_datasets.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_deployments.py +14 -15
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_evals.py +3 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_optimizations.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_parity.py +21 -22
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_programs.py +4 -4
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_schema_sample.py +7 -7
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_shell.py +82 -82
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_signatures.py +5 -5
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_traces.py +16 -17
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_traces_search_wire.py +18 -17
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_valsets.py +2 -2
- cmpnd-0.23.1/tests/test_client.py +722 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cmp551_grouping_identity.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_deploy_mcp_tool_references.py +6 -7
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_exporter.py +17 -19
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_ir_encoder_properties.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_mcp_tools.py +66 -64
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimization_progress.py +11 -11
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_terminal_failure.py +2 -2
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_packaging_mcp_tools.py +4 -4
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_reflector_properties.py +4 -4
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_reflector_smoke.py +3 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_reversible_failures.py +3 -4
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_traces_read.py +16 -13
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_xxh64.py +3 -3
- {cmpnd-0.22.0 → cmpnd-0.23.1}/uv.lock +31 -31
- cmpnd-0.22.0/cmpnd/_gepa_patch.py +0 -185
- {cmpnd-0.22.0 → cmpnd-0.23.1}/.gitignore +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/CLAUDE.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/README.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/_http.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/configuration.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/context.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/datasets.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/decorators.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/deployment.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/execution.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/exporter.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/helpers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/identity.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/imaging.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/models.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimization.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/retry.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/cmpnd/traces.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/cli.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/custom-proposer.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/docs/reading-traces.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/examples/local_ollama.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/README.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/ingest/pyproject.toml +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/_fake_clock.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/__main__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/drain.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/ingest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/sweep.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_health.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/_provider.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/_result.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_callback.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_configuration.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_context.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_datasets.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_decorators.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_deploy.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_eval.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_eval_start.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_execute.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_identity.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_imaging.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_import_weight.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_integration.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_models.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_packaging.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_poll_until.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_project_payload.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_retry.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_schemas.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_trace_render.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/tests/test_truncation.py +0 -0
- {cmpnd-0.22.0 → cmpnd-0.23.1}/todo.md +0 -0
|
@@ -17,6 +17,7 @@ import importlib.metadata
|
|
|
17
17
|
from . import traces
|
|
18
18
|
from ._http import ApiError
|
|
19
19
|
from .callback import CmpndCallback, trace_id
|
|
20
|
+
from .client import Client
|
|
20
21
|
from .configuration import configure, get_config, require_config
|
|
21
22
|
from .context import (
|
|
22
23
|
clear_context,
|
|
@@ -48,6 +49,11 @@ try:
|
|
|
48
49
|
except importlib.metadata.PackageNotFoundError:
|
|
49
50
|
__version__ = "0.0.0"
|
|
50
51
|
|
|
52
|
+
# The exact callback auto_instrument() installed, so de_instrument() removes that
|
|
53
|
+
# one and leaves a manually installed CmpndCallback alone. None when this SDK owns
|
|
54
|
+
# no global callback.
|
|
55
|
+
_auto_instrument_callback: CmpndCallback | None = None
|
|
56
|
+
|
|
51
57
|
__all__ = [
|
|
52
58
|
# Configuration
|
|
53
59
|
"configure",
|
|
@@ -57,6 +63,9 @@ __all__ = [
|
|
|
57
63
|
"CmpndCallback",
|
|
58
64
|
# Auto-instrumentation
|
|
59
65
|
"auto_instrument",
|
|
66
|
+
"de_instrument",
|
|
67
|
+
# Scoped, membership-based tracing
|
|
68
|
+
"Client",
|
|
60
69
|
# Manual tracing
|
|
61
70
|
"trace",
|
|
62
71
|
"start_span",
|
|
@@ -139,10 +148,15 @@ def auto_instrument() -> CmpndCallback:
|
|
|
139
148
|
# Create callback
|
|
140
149
|
callback = CmpndCallback()
|
|
141
150
|
|
|
142
|
-
# Get current callbacks and avoid duplicates
|
|
151
|
+
# Get current callbacks and avoid duplicates. Remember the exact instance this
|
|
152
|
+
# call installs so de_instrument() removes only it — never a CmpndCallback an
|
|
153
|
+
# application installed itself. When one is already present (this call or a
|
|
154
|
+
# manual install), we add nothing and own nothing to remove.
|
|
155
|
+
global _auto_instrument_callback
|
|
143
156
|
current_callbacks = dspy.settings.get("callbacks", []) or []
|
|
144
157
|
if not any(isinstance(cb, CmpndCallback) for cb in current_callbacks):
|
|
145
158
|
dspy.configure(callbacks=[*current_callbacks, callback])
|
|
159
|
+
_auto_instrument_callback = callback
|
|
146
160
|
|
|
147
161
|
# Patch GEPA for automatic optimization tracking
|
|
148
162
|
from . import _gepa_patch
|
|
@@ -163,3 +177,35 @@ def auto_instrument() -> CmpndCallback:
|
|
|
163
177
|
_rlm_patch.patch_rlm_tools()
|
|
164
178
|
|
|
165
179
|
return callback
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def de_instrument() -> None:
|
|
183
|
+
"""Remove global auto_instrument() instrumentation and restore patched classes.
|
|
184
|
+
|
|
185
|
+
The full teardown counterpart to :func:`auto_instrument`: removes the exact
|
|
186
|
+
``CmpndCallback`` that :func:`auto_instrument` installed and restores
|
|
187
|
+
``dspy.GEPA``, ``dspy.RLM`` and the program-state ``dump_state`` /
|
|
188
|
+
``load_state`` to pristine. Idempotent, a no-op when nothing is instrumented,
|
|
189
|
+
and it touches no :class:`Client` — a client's callback is scoped through
|
|
190
|
+
``dspy.context`` and never lives in the global settings this removes. It also
|
|
191
|
+
leaves a ``CmpndCallback`` an application installed itself in place, removing
|
|
192
|
+
only the one this SDK owns.
|
|
193
|
+
"""
|
|
194
|
+
global _auto_instrument_callback
|
|
195
|
+
try:
|
|
196
|
+
import dspy
|
|
197
|
+
except ImportError:
|
|
198
|
+
dspy = None
|
|
199
|
+
|
|
200
|
+
if dspy is not None and _auto_instrument_callback is not None:
|
|
201
|
+
current = list(dspy.settings.get("callbacks", []) or [])
|
|
202
|
+
remaining = [cb for cb in current if cb is not _auto_instrument_callback]
|
|
203
|
+
if len(remaining) != len(current):
|
|
204
|
+
dspy.configure(callbacks=remaining)
|
|
205
|
+
_auto_instrument_callback = None
|
|
206
|
+
|
|
207
|
+
from . import _gepa_patch, _program_patch, _rlm_patch
|
|
208
|
+
|
|
209
|
+
_gepa_patch.unpatch_gepa()
|
|
210
|
+
_program_patch.unpatch_program_state()
|
|
211
|
+
_rlm_patch.unpatch_rlm_tools()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Exporter pool for client-scoped tracing, keyed by credentials.
|
|
2
|
+
|
|
3
|
+
A ``Client`` owns config and a callback, never an OS resource. The one
|
|
4
|
+
long-lived resource in the trace-production path is the exporter's background
|
|
5
|
+
thread, so clients do not each hold one: exporters live here at module scope,
|
|
6
|
+
keyed by ``(endpoint, api_key)``, started on first use and flushed at process
|
|
7
|
+
exit. A client that falls out of scope or is deleted releases nothing; its
|
|
8
|
+
queued traces still drain through the pooled exporter's ``atexit`` flush.
|
|
9
|
+
|
|
10
|
+
The pool honors a process-injected exporter. A worker replaces
|
|
11
|
+
``cmpnd.exporter._exporter`` with an in-memory collector to read traces back
|
|
12
|
+
with no server, and a test injects a recorder the same way; when the process has
|
|
13
|
+
set one, every client publishes through it rather than opening its own. Only when
|
|
14
|
+
no exporter is injected does a client get a pooled transport of its own.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import atexit
|
|
20
|
+
import threading
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from . import configuration, exporter
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from . import exporter_http
|
|
27
|
+
|
|
28
|
+
_pool: dict[tuple[str, str], exporter_http.BatchExporter] = {}
|
|
29
|
+
_pool_lock = threading.Lock()
|
|
30
|
+
_atexit_registered = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _key(config: configuration.Config) -> tuple[str, str]:
|
|
34
|
+
return (config.endpoint, config.api_key)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _pooled(config: configuration.Config) -> exporter_http.BatchExporter:
|
|
38
|
+
"""Return the exporter for these credentials, starting it once."""
|
|
39
|
+
global _atexit_registered
|
|
40
|
+
with _pool_lock:
|
|
41
|
+
key = _key(config)
|
|
42
|
+
exp = _pool.get(key)
|
|
43
|
+
if exp is None:
|
|
44
|
+
from . import exporter_http
|
|
45
|
+
|
|
46
|
+
exp = exporter_http.BatchExporter(config)
|
|
47
|
+
exp.start()
|
|
48
|
+
_pool[key] = exp
|
|
49
|
+
if not _atexit_registered:
|
|
50
|
+
atexit.register(flush_all)
|
|
51
|
+
_atexit_registered = True
|
|
52
|
+
return exp
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_exporter(config: configuration.Config) -> Any:
|
|
56
|
+
"""The exporter a client with this config publishes through.
|
|
57
|
+
|
|
58
|
+
A genuine process injection wins; otherwise the pooled transport for these
|
|
59
|
+
credentials. Returns ``None`` only when nothing is injected and the pooled
|
|
60
|
+
transport has not been created — a caller that only reads never forces one.
|
|
61
|
+
"""
|
|
62
|
+
injected = _worker_injection()
|
|
63
|
+
if injected is not None:
|
|
64
|
+
return injected
|
|
65
|
+
with _pool_lock:
|
|
66
|
+
return _pool.get(_key(config))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ensure_exporter_started(config: configuration.Config) -> Any:
|
|
70
|
+
"""Return this config's exporter, creating the pooled transport if needed."""
|
|
71
|
+
injected = _worker_injection()
|
|
72
|
+
if injected is not None:
|
|
73
|
+
return injected
|
|
74
|
+
return _pooled(config)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _worker_injection() -> Any:
|
|
78
|
+
"""A worker's directly-injected collector, or None.
|
|
79
|
+
|
|
80
|
+
A worker replaces ``cmpnd.exporter._exporter`` with an in-memory collector and
|
|
81
|
+
never calls ``cmpnd.configure()`` — it is the single tenant, so every trace
|
|
82
|
+
belongs in that collector. ``auto_instrument`` also sets a process exporter,
|
|
83
|
+
but through ``configure()``, and that one must NOT capture a client's traces:
|
|
84
|
+
a client points at its own endpoint with its own key. So an injected exporter
|
|
85
|
+
counts as a worker's only when no cmpnd config is set; once ``configure()``
|
|
86
|
+
has run, a client publishes through its own credential-keyed pool instead.
|
|
87
|
+
"""
|
|
88
|
+
injected = exporter.get_exporter()
|
|
89
|
+
if injected is not None and configuration.get_config() is None:
|
|
90
|
+
return injected
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def flush(config: configuration.Config, timeout: float = 5.0) -> bool:
|
|
95
|
+
"""Drain this config's exporter so a just-written trace is visible.
|
|
96
|
+
|
|
97
|
+
Returns ``True`` when the drain completed, ``False`` when it timed out or
|
|
98
|
+
there is nothing to drain.
|
|
99
|
+
"""
|
|
100
|
+
exp = get_exporter(config)
|
|
101
|
+
if exp is not None and hasattr(exp, "flush"):
|
|
102
|
+
return bool(exp.flush(timeout))
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def flush_all(timeout: float = 5.0) -> None:
|
|
107
|
+
"""Flush every pooled exporter — the ``atexit`` drain."""
|
|
108
|
+
with _pool_lock:
|
|
109
|
+
exporters = list(_pool.values())
|
|
110
|
+
for exp in exporters:
|
|
111
|
+
try:
|
|
112
|
+
exp.flush(timeout)
|
|
113
|
+
except Exception: # noqa: BLE001 — best-effort drain at shutdown
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _reset_for_tests() -> None: # pyright: ignore[reportUnusedFunction] # called from tests/, outside pyright's include
|
|
118
|
+
"""Drop pooled exporters. Test-only; the pool is otherwise process-lived."""
|
|
119
|
+
with _pool_lock:
|
|
120
|
+
_pool.clear()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""dspy.GEPA monkey-patch for automatic optimization tracking.
|
|
2
|
+
|
|
3
|
+
Extracted from ``cmpnd/__init__.py`` so its first-party imports can be
|
|
4
|
+
module-style (``from . import identity``); basedpyright can't resolve
|
|
5
|
+
``from . import <submodule>`` inside the package's own ``__init__``.
|
|
6
|
+
|
|
7
|
+
``patch_gepa()`` is invoked lazily from ``cmpnd.auto_instrument()`` so importing
|
|
8
|
+
``cmpnd`` doesn't drag in dspy / the optimizers (and transitively litellm).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import contextvars
|
|
12
|
+
|
|
13
|
+
from . import context
|
|
14
|
+
|
|
15
|
+
_gepa_patched = False
|
|
16
|
+
|
|
17
|
+
# Set by a client's run_optimization while it owns the run. The class patch reads
|
|
18
|
+
# it at compile entry: when a client owns the optimization, the patch does not
|
|
19
|
+
# also track it — the client records the run on its own account, and a second
|
|
20
|
+
# global-account record would both misattribute and duplicate it.
|
|
21
|
+
_client_owns_optimization: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
22
|
+
"cmpnd_client_owns_optimization", default=False
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _gepa_supports_callbacks() -> bool:
|
|
27
|
+
"""Check if the installed gepa version supports callbacks in optimize()."""
|
|
28
|
+
try:
|
|
29
|
+
import inspect
|
|
30
|
+
|
|
31
|
+
from gepa import api
|
|
32
|
+
|
|
33
|
+
return "callbacks" in inspect.signature(api.optimize).parameters
|
|
34
|
+
except Exception:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_tracked_compile(
|
|
39
|
+
gepa, student, *, trainset, valset, run_original, has_callbacks=None, project_tags=None, exporter_override=None
|
|
40
|
+
):
|
|
41
|
+
"""Track one GEPA compile: mint a tracker, start, run the search, finalize.
|
|
42
|
+
|
|
43
|
+
Shared by the auto_instrument class patch (global account) and a client's
|
|
44
|
+
run_optimization (the client's tags + its own transport). ``run_original``
|
|
45
|
+
runs the actual search and returns the compiled program. ``project_tags`` and
|
|
46
|
+
``exporter_override`` scope the run record to a client when given.
|
|
47
|
+
``has_callbacks`` is passed by the patch (resolved once at patch time); a
|
|
48
|
+
client leaves it None and it is resolved here.
|
|
49
|
+
"""
|
|
50
|
+
import dspy
|
|
51
|
+
|
|
52
|
+
from .optimizers import gepa_callback, tracker
|
|
53
|
+
|
|
54
|
+
if has_callbacks is None:
|
|
55
|
+
has_callbacks = _gepa_supports_callbacks()
|
|
56
|
+
|
|
57
|
+
trk = tracker.OptimizationTracker(optimizer_type="gepa", program_name="")
|
|
58
|
+
if project_tags is not None:
|
|
59
|
+
trk.project_tags = project_tags
|
|
60
|
+
trk._exporter_override = exporter_override
|
|
61
|
+
callback = gepa_callback.CmpndGEPACallback(trk, defer_finalize=True)
|
|
62
|
+
|
|
63
|
+
if has_callbacks:
|
|
64
|
+
old_callback = getattr(gepa, "_cmpnd_gepa_callback", None)
|
|
65
|
+
existing = gepa.gepa_kwargs.get("callbacks") or []
|
|
66
|
+
if old_callback is not None:
|
|
67
|
+
existing = [cb for cb in existing if cb is not old_callback]
|
|
68
|
+
gepa.gepa_kwargs["callbacks"] = existing + [callback]
|
|
69
|
+
gepa._cmpnd_gepa_callback = callback
|
|
70
|
+
|
|
71
|
+
trk = callback._tracker
|
|
72
|
+
|
|
73
|
+
# Build the config dict from the GEPA instance, then hand the live program to
|
|
74
|
+
# the shared enrichment helper (one definition of the optimization_run payload
|
|
75
|
+
# shape, reused by the deploy path).
|
|
76
|
+
config = {}
|
|
77
|
+
for key in ["auto", "max_metric_calls", "max_full_evals", "seed", "log_dir"]:
|
|
78
|
+
val = getattr(gepa, key, None)
|
|
79
|
+
if val is not None:
|
|
80
|
+
config[key] = val
|
|
81
|
+
|
|
82
|
+
lm = None
|
|
83
|
+
try:
|
|
84
|
+
lm = dspy.settings.lm
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# For an `auto` preset max_metric_calls is None until GEPA's first budget
|
|
89
|
+
# event, so estimate the up-front budget now for the initial "running" record.
|
|
90
|
+
max_metric_calls = getattr(gepa, "max_metric_calls", None)
|
|
91
|
+
if not max_metric_calls:
|
|
92
|
+
max_metric_calls = tracker.estimate_max_metric_calls(
|
|
93
|
+
student=student,
|
|
94
|
+
trainset=trainset,
|
|
95
|
+
valset=valset,
|
|
96
|
+
max_metric_calls=max_metric_calls,
|
|
97
|
+
max_full_evals=getattr(gepa, "max_full_evals", None),
|
|
98
|
+
auto=getattr(gepa, "auto", None),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
tracker.enrich_from_program(
|
|
102
|
+
trk,
|
|
103
|
+
student=student,
|
|
104
|
+
trainset=trainset,
|
|
105
|
+
valset=valset,
|
|
106
|
+
lm=lm,
|
|
107
|
+
reflection_lm=getattr(gepa, "reflection_lm", None),
|
|
108
|
+
config=config,
|
|
109
|
+
metric_fn=getattr(gepa, "metric_fn", None) or getattr(gepa, "metric", None),
|
|
110
|
+
max_metric_calls=max_metric_calls,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
trk.start()
|
|
114
|
+
context.set_current_optimization_run_id(trk.optimization_run_id)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = run_original()
|
|
118
|
+
|
|
119
|
+
if not has_callbacks:
|
|
120
|
+
detailed = getattr(result, "detailed_results", None)
|
|
121
|
+
if detailed is not None:
|
|
122
|
+
trk.capture_final_results(detailed)
|
|
123
|
+
|
|
124
|
+
trk.capture_compiled_program(result)
|
|
125
|
+
trk.finalize()
|
|
126
|
+
|
|
127
|
+
# Tag the compiled program with the run id, after finalize (the last point
|
|
128
|
+
# the run can acquire one), so the compiled-from link points at a run that
|
|
129
|
+
# exists.
|
|
130
|
+
if hasattr(result, "__dict__"):
|
|
131
|
+
result._optimization_run_id = str(trk.optimization_run_id)
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
# BaseException: a run that ends by ANY route must stop claiming progress on
|
|
136
|
+
# the row start() sent, and a backend model-refusal raises outside Exception.
|
|
137
|
+
except BaseException as e:
|
|
138
|
+
trk.finalize_error(e)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
finally:
|
|
142
|
+
context.set_current_optimization_run_id(None)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def patch_gepa() -> None:
|
|
146
|
+
"""Monkey-patch dspy.GEPA to auto-inject CmpndGEPACallback.
|
|
147
|
+
|
|
148
|
+
Patches both __init__ (to create callback/tracker shell) and compile
|
|
149
|
+
(to enrich tracker with student/valset info before optimization runs).
|
|
150
|
+
|
|
151
|
+
Uses a module-level flag to avoid double-patching.
|
|
152
|
+
"""
|
|
153
|
+
global _gepa_patched
|
|
154
|
+
if _gepa_patched:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
import dspy
|
|
158
|
+
|
|
159
|
+
if not hasattr(dspy, "GEPA"):
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Resolve callback support once, at patch time — not per compile — so the
|
|
163
|
+
# answer matches the installed gepa the patch bound to.
|
|
164
|
+
has_callbacks = _gepa_supports_callbacks()
|
|
165
|
+
|
|
166
|
+
GEPA = dspy.GEPA
|
|
167
|
+
GEPA._cmpnd_original_init = GEPA.__init__ # pyright: ignore[reportAttributeAccessIssue]
|
|
168
|
+
GEPA._cmpnd_original_compile = GEPA.compile # pyright: ignore[reportAttributeAccessIssue]
|
|
169
|
+
|
|
170
|
+
def _patched_init(self, *args, **kwargs):
|
|
171
|
+
# Force track_stats=True so detailed_results is always available
|
|
172
|
+
if "track_stats" not in kwargs:
|
|
173
|
+
kwargs["track_stats"] = True
|
|
174
|
+
|
|
175
|
+
type(self)._cmpnd_original_init(self, *args, **kwargs)
|
|
176
|
+
|
|
177
|
+
def _patched_compile(self, student, *, trainset, teacher=None, valset=None, **kwargs):
|
|
178
|
+
# When a client's run_optimization owns this run it records the run on its
|
|
179
|
+
# own account and has suppressed the global track; run the search only.
|
|
180
|
+
if _client_owns_optimization.get():
|
|
181
|
+
return type(self)._cmpnd_original_compile(
|
|
182
|
+
self, student, trainset=trainset, teacher=teacher, valset=valset, **kwargs
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Otherwise track the run on the global account, the same body a client's
|
|
186
|
+
# run_optimization drives with its own tags and transport.
|
|
187
|
+
def _search():
|
|
188
|
+
return type(self)._cmpnd_original_compile(
|
|
189
|
+
self, student, trainset=trainset, teacher=teacher, valset=valset, **kwargs
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return _run_tracked_compile(
|
|
193
|
+
self, student, trainset=trainset, valset=valset, run_original=_search, has_callbacks=has_callbacks
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
GEPA.__init__ = _patched_init
|
|
197
|
+
GEPA.compile = _patched_compile
|
|
198
|
+
_gepa_patched = True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def unpatch_gepa() -> None:
|
|
202
|
+
"""Restore ``dspy.GEPA.__init__`` / ``.compile`` to pristine. Idempotent."""
|
|
203
|
+
global _gepa_patched
|
|
204
|
+
try:
|
|
205
|
+
import dspy
|
|
206
|
+
except ImportError:
|
|
207
|
+
_gepa_patched = False
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
GEPA = getattr(dspy, "GEPA", None)
|
|
211
|
+
if GEPA is not None and hasattr(GEPA, "_cmpnd_original_init"):
|
|
212
|
+
GEPA.__init__ = GEPA._cmpnd_original_init
|
|
213
|
+
GEPA.compile = GEPA._cmpnd_original_compile
|
|
214
|
+
del GEPA._cmpnd_original_init
|
|
215
|
+
del GEPA._cmpnd_original_compile
|
|
216
|
+
_gepa_patched = False
|
|
@@ -40,6 +40,9 @@ _LINEAGE_KEY = "_cmpnd"
|
|
|
40
40
|
_RUN_ID_FIELD = "optimization_run_id"
|
|
41
41
|
|
|
42
42
|
_patched = False
|
|
43
|
+
# The original dump_state/load_state per patched class, stashed so de_instrument
|
|
44
|
+
# can restore each to pristine. A list of ``(class, name, method)`` or None.
|
|
45
|
+
_originals: list[tuple[Any, str, Any]] | None = None
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
def _inject(state: Any, module: Any) -> Any:
|
|
@@ -89,7 +92,7 @@ def patch_program_state() -> None:
|
|
|
89
92
|
Idempotent (a module-level flag guards against double-patching); a no-op
|
|
90
93
|
when dspy isn't installed.
|
|
91
94
|
"""
|
|
92
|
-
global _patched
|
|
95
|
+
global _patched, _originals
|
|
93
96
|
if _patched:
|
|
94
97
|
return
|
|
95
98
|
|
|
@@ -105,8 +108,21 @@ def patch_program_state() -> None:
|
|
|
105
108
|
# assignment because dspy.*.load_state types as Unknown to basedpyright (its
|
|
106
109
|
# body imports the untyped dspy.predict.predict) — reflection keeps the swap
|
|
107
110
|
# off the type checker's attribute-access path across every DSPy version.
|
|
111
|
+
_originals = []
|
|
108
112
|
for cls, strip in ((dspy.Module, False), (dspy.Predict, True)):
|
|
113
|
+
for name in ("dump_state", "load_state"):
|
|
114
|
+
_originals.append((cls, name, getattr(cls, name)))
|
|
109
115
|
setattr(cls, "dump_state", _wrap_dump(getattr(cls, "dump_state")))
|
|
110
116
|
setattr(cls, "load_state", _wrap_load(getattr(cls, "load_state"), strip=strip))
|
|
111
117
|
|
|
112
118
|
_patched = True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def unpatch_program_state() -> None:
|
|
122
|
+
"""Restore dump_state/load_state on the patched classes to pristine. Idempotent."""
|
|
123
|
+
global _patched, _originals
|
|
124
|
+
if _originals is not None:
|
|
125
|
+
for cls, name, method in _originals:
|
|
126
|
+
setattr(cls, name, method)
|
|
127
|
+
_originals = None
|
|
128
|
+
_patched = False
|
|
@@ -53,6 +53,9 @@ import uuid
|
|
|
53
53
|
from typing import Any, Callable
|
|
54
54
|
|
|
55
55
|
_patched = False
|
|
56
|
+
# The original ``RLM._prepare_execution_tools``, stashed so de_instrument can
|
|
57
|
+
# restore the class to pristine. ``(class, method)`` or None when unpatched.
|
|
58
|
+
_original: tuple[Any, Callable[..., Any]] | None = None
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
def _active_cmpnd_callback() -> Any | None:
|
|
@@ -118,7 +121,7 @@ def patch_rlm_tools() -> None:
|
|
|
118
121
|
Idempotent (a module-level flag guards double-patching); a no-op when dspy
|
|
119
122
|
or ``dspy.RLM`` isn't available.
|
|
120
123
|
"""
|
|
121
|
-
global _patched
|
|
124
|
+
global _patched, _original
|
|
122
125
|
if _patched:
|
|
123
126
|
return
|
|
124
127
|
|
|
@@ -132,6 +135,7 @@ def patch_rlm_tools() -> None:
|
|
|
132
135
|
return
|
|
133
136
|
|
|
134
137
|
orig = getattr(rlm_cls, "_prepare_execution_tools")
|
|
138
|
+
_original = (rlm_cls, orig)
|
|
135
139
|
|
|
136
140
|
@functools.wraps(orig)
|
|
137
141
|
def _prepare_execution_tools(self: Any) -> dict[str, Callable[..., Any]]:
|
|
@@ -144,3 +148,13 @@ def patch_rlm_tools() -> None:
|
|
|
144
148
|
|
|
145
149
|
setattr(rlm_cls, "_prepare_execution_tools", _prepare_execution_tools)
|
|
146
150
|
_patched = True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def unpatch_rlm_tools() -> None:
|
|
154
|
+
"""Restore ``RLM._prepare_execution_tools`` to pristine. Idempotent."""
|
|
155
|
+
global _patched, _original
|
|
156
|
+
if _original is not None:
|
|
157
|
+
rlm_cls, orig = _original
|
|
158
|
+
setattr(rlm_cls, "_prepare_execution_tools", orig)
|
|
159
|
+
_original = None
|
|
160
|
+
_patched = False
|
|
@@ -283,9 +283,19 @@ class _CallbackBase:
|
|
|
283
283
|
max_input_field_size: int | None = None,
|
|
284
284
|
max_output_field_size: int | None = None,
|
|
285
285
|
stream_trace_starts: bool | None = None,
|
|
286
|
+
own_config: configuration.Config | None = None,
|
|
286
287
|
) -> None:
|
|
288
|
+
# A client passes its own config here; the auto_instrument callback leaves
|
|
289
|
+
# it None and reads the process-global config live, so a reconfigure()
|
|
290
|
+
# after this callback is built still changes what the global path emits.
|
|
291
|
+
# A client's config, by contrast, is fixed at construction: the tags,
|
|
292
|
+
# credentials and endpoint it stamps and publishes through never follow a
|
|
293
|
+
# later cmpnd.configure(). That fixedness is what keeps a client
|
|
294
|
+
# orthogonal to the global config.
|
|
295
|
+
self._own_config = own_config
|
|
296
|
+
|
|
287
297
|
# Resolve feature flags: explicit param > config > default
|
|
288
|
-
config = configuration.get_config()
|
|
298
|
+
config = own_config or configuration.get_config()
|
|
289
299
|
self._capture_inputs = (
|
|
290
300
|
capture_inputs if capture_inputs is not None else (config.capture_inputs if config else True)
|
|
291
301
|
)
|
|
@@ -368,10 +378,53 @@ class _CallbackBase:
|
|
|
368
378
|
self._interpreter_lifecycle_starts: dict[str, datetime] = {}
|
|
369
379
|
|
|
370
380
|
# Ensure exporter is running
|
|
371
|
-
|
|
381
|
+
self._ensure_exporter()
|
|
382
|
+
|
|
383
|
+
def _resolve_project_tags(self) -> list[str] | None:
|
|
384
|
+
"""The project tags this callback stamps on the builders it creates.
|
|
385
|
+
|
|
386
|
+
A client callback stamps its own fixed tags. The auto_instrument callback
|
|
387
|
+
reads the live global config, so a reconfigure() after it was built still
|
|
388
|
+
changes the tags it emits — the behavior the builders' factory default
|
|
389
|
+
already gives, kept here so both paths resolve tags one way.
|
|
390
|
+
"""
|
|
391
|
+
if self._own_config is not None:
|
|
392
|
+
return self._own_config.project_tags
|
|
393
|
+
return configuration._current_project_tags()
|
|
394
|
+
|
|
395
|
+
def _stamp(self, builder: Any) -> Any:
|
|
396
|
+
"""Stamp this callback's project tags onto a builder it just created."""
|
|
397
|
+
builder.project_tags = self._resolve_project_tags()
|
|
398
|
+
return builder
|
|
399
|
+
|
|
400
|
+
def _current_exporter(self) -> Any:
|
|
401
|
+
"""The exporter this callback publishes through.
|
|
402
|
+
|
|
403
|
+
A client publishes through the pool keyed by its own credentials; the
|
|
404
|
+
auto_instrument callback publishes through the process-global exporter.
|
|
405
|
+
Both honor a worker's injected in-memory collector. A client creates its
|
|
406
|
+
pooled transport on demand here, not only at construction: whether it owns
|
|
407
|
+
one depends on the config state (a worker injection vs a cmpnd config),
|
|
408
|
+
and that state can change between constructing the client and its first
|
|
409
|
+
publish — so the publish path resolves and creates, never just reads.
|
|
410
|
+
"""
|
|
411
|
+
if self._own_config is not None:
|
|
412
|
+
from . import _exporter_pool
|
|
413
|
+
|
|
414
|
+
return _exporter_pool.ensure_exporter_started(self._own_config)
|
|
415
|
+
return exporter.get_exporter()
|
|
416
|
+
|
|
417
|
+
def _ensure_exporter(self) -> Any:
|
|
418
|
+
"""Start this callback's exporter if it is not already running."""
|
|
419
|
+
if self._own_config is not None:
|
|
420
|
+
from . import _exporter_pool
|
|
421
|
+
|
|
422
|
+
return _exporter_pool.ensure_exporter_started(self._own_config)
|
|
423
|
+
return exporter.ensure_exporter_started()
|
|
372
424
|
|
|
373
425
|
def _track_span(self, span: models.SpanBuilder) -> None:
|
|
374
426
|
"""Register a span under its trace for batch payload assembly."""
|
|
427
|
+
self._stamp(span)
|
|
375
428
|
if span.trace_id:
|
|
376
429
|
self._trace_spans.setdefault(str(span.trace_id), []).append(span)
|
|
377
430
|
|
|
@@ -444,7 +497,7 @@ class _ModuleMixin(_CallbackBase):
|
|
|
444
497
|
# exporter coalesces a start with its batch trace in one flush, so a
|
|
445
498
|
# fast trace pays a single insert, not two.
|
|
446
499
|
if self._stream_trace_starts:
|
|
447
|
-
exp =
|
|
500
|
+
exp = self._current_exporter()
|
|
448
501
|
if exp:
|
|
449
502
|
exp.export_trace_start(trace)
|
|
450
503
|
|
|
@@ -604,7 +657,7 @@ class _ModuleMixin(_CallbackBase):
|
|
|
604
657
|
|
|
605
658
|
# Export span (only for streaming traces)
|
|
606
659
|
if self._should_stream_span(span):
|
|
607
|
-
exp =
|
|
660
|
+
exp = self._current_exporter()
|
|
608
661
|
if exp:
|
|
609
662
|
exp.export_span(span)
|
|
610
663
|
|
|
@@ -661,7 +714,7 @@ class _ModuleMixin(_CallbackBase):
|
|
|
661
714
|
f"Trace collected for eval: tokens={trace.total_tokens}, cost={trace.total_cost:.4f}"
|
|
662
715
|
)
|
|
663
716
|
|
|
664
|
-
exp =
|
|
717
|
+
exp = self._current_exporter()
|
|
665
718
|
if exp:
|
|
666
719
|
if trace.streaming:
|
|
667
720
|
exp.export_trace_finalize(trace) # Finalize the streaming trace
|
|
@@ -784,6 +837,7 @@ class _ModuleMixin(_CallbackBase):
|
|
|
784
837
|
start_time=datetime.now(timezone.utc),
|
|
785
838
|
program_name=instance.__class__.__name__,
|
|
786
839
|
)
|
|
840
|
+
self._stamp(trace)
|
|
787
841
|
|
|
788
842
|
# Capture signature info for the root module - this identifies the program
|
|
789
843
|
signature = None
|
|
@@ -1056,7 +1110,7 @@ class _LMMixin(_CallbackBase):
|
|
|
1056
1110
|
self._trace_spans[trace_id_str].append(span)
|
|
1057
1111
|
|
|
1058
1112
|
if self._should_stream_span(span):
|
|
1059
|
-
exp =
|
|
1113
|
+
exp = self._current_exporter()
|
|
1060
1114
|
if exp:
|
|
1061
1115
|
exp.export_span(span)
|
|
1062
1116
|
|
|
@@ -1099,6 +1153,7 @@ class _LMMixin(_CallbackBase):
|
|
|
1099
1153
|
end_time=span.end_time,
|
|
1100
1154
|
program_name="GEPA",
|
|
1101
1155
|
)
|
|
1156
|
+
self._stamp(trace)
|
|
1102
1157
|
# A metric-phase call can start while DSPy's Evaluate span is still on the
|
|
1103
1158
|
# context stack, so on_lm_start stamped this span with a parent_span_id for
|
|
1104
1159
|
# a span that isn't in this standalone one-span trace. This span IS the
|
|
@@ -1119,7 +1174,7 @@ class _LMMixin(_CallbackBase):
|
|
|
1119
1174
|
trace.total_cost = span.cost or 0.0
|
|
1120
1175
|
trace.span_count = 1
|
|
1121
1176
|
|
|
1122
|
-
exp =
|
|
1177
|
+
exp = self._current_exporter()
|
|
1123
1178
|
if exp:
|
|
1124
1179
|
trace._batch_spans = [span]
|
|
1125
1180
|
exp.export_trace(trace)
|
|
@@ -1218,7 +1273,7 @@ class _AdapterMixin(_CallbackBase):
|
|
|
1218
1273
|
self._trace_spans[trace_id_str].append(span)
|
|
1219
1274
|
|
|
1220
1275
|
if self._should_stream_span(span):
|
|
1221
|
-
exp =
|
|
1276
|
+
exp = self._current_exporter()
|
|
1222
1277
|
if exp:
|
|
1223
1278
|
exp.export_span(span)
|
|
1224
1279
|
|
|
@@ -1300,7 +1355,7 @@ class _ToolMixin(_CallbackBase):
|
|
|
1300
1355
|
self._track_span(span)
|
|
1301
1356
|
|
|
1302
1357
|
if self._should_stream_span(span):
|
|
1303
|
-
exp =
|
|
1358
|
+
exp = self._current_exporter()
|
|
1304
1359
|
if exp:
|
|
1305
1360
|
exp.export_span(span)
|
|
1306
1361
|
|
|
@@ -1375,7 +1430,7 @@ class _InterpreterMixin(_CallbackBase):
|
|
|
1375
1430
|
self._track_span(span)
|
|
1376
1431
|
|
|
1377
1432
|
if self._should_stream_span(span):
|
|
1378
|
-
exp =
|
|
1433
|
+
exp = self._current_exporter()
|
|
1379
1434
|
if exp:
|
|
1380
1435
|
exp.export_span(span)
|
|
1381
1436
|
|
|
@@ -1467,7 +1522,7 @@ class _InterpreterMixin(_CallbackBase):
|
|
|
1467
1522
|
|
|
1468
1523
|
self._track_span(span)
|
|
1469
1524
|
if self._should_stream_span(span):
|
|
1470
|
-
exp =
|
|
1525
|
+
exp = self._current_exporter()
|
|
1471
1526
|
if exp:
|
|
1472
1527
|
exp.export_span(span)
|
|
1473
1528
|
|