cmpnd 0.4.1__tar.gz → 0.4.2__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.4.1 → cmpnd-0.4.2}/PKG-INFO +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/__init__.py +6 -0
- cmpnd-0.4.2/cmpnd/_rlm_patch.py +134 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/pyproject.toml +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/seed_review_run.py +118 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/conftest.py +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_deploy_programs.py +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_multi_client_stress.py +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_real_provider_twins.py +1 -1
- cmpnd-0.4.2/tests/test_rlm_patch.py +180 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/uv.lock +1 -1
- {cmpnd-0.4.1 → cmpnd-0.4.2}/.gitignore +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/CLAUDE.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/README.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/callback.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/configuration.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/context.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/datasets.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/decorators.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/deployment.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/execution.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/exporter.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/helpers.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/identity.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/models.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimization.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/packaging.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/sdk-instrumentation.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/examples/local_ollama.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/conftest.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/conftest.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_health.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_callback.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_configuration.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_context.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_datasets.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_decorators.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_deploy.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_eval.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_eval_start.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_execute.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exporter.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_identity.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_integration.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_models.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_project_payload.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_schemas.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_stats_export.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_truncation.py +0 -0
- {cmpnd-0.4.1 → cmpnd-0.4.2}/todo.md +0 -0
|
@@ -136,4 +136,10 @@ def auto_instrument() -> CmpndCallback:
|
|
|
136
136
|
|
|
137
137
|
_program_patch.patch_program_state()
|
|
138
138
|
|
|
139
|
+
# Emit tool spans for dspy.RLM tool calls, which otherwise bypass
|
|
140
|
+
# dspy.Tool.__call__'s callbacks and produce no spans (CMP-187).
|
|
141
|
+
from . import _rlm_patch
|
|
142
|
+
|
|
143
|
+
_rlm_patch.patch_rlm_tools()
|
|
144
|
+
|
|
139
145
|
return callback
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Emit tool spans for ``dspy.RLM`` tool calls (CMP-187).
|
|
2
|
+
|
|
3
|
+
cmpnd creates a ``TOOL`` span only from DSPy's ``on_tool_start``/``on_tool_end``
|
|
4
|
+
callbacks, and those fire only through ``dspy.Tool.__call__`` (decorated
|
|
5
|
+
``@with_callbacks``). ``dspy.RLM._prepare_execution_tools`` hands the code
|
|
6
|
+
interpreter the **bare** ``tool.func`` for each user tool
|
|
7
|
+
(``{name: tool.func for name, tool in self._user_tools.items()}`` in
|
|
8
|
+
``dspy/predict/rlm.py``), so ``Tool.__call__`` is bypassed and no span is
|
|
9
|
+
created — unlike ``dspy.ReAct``, which calls the ``Tool`` object and gets tool
|
|
10
|
+
spans for free.
|
|
11
|
+
|
|
12
|
+
``patch_rlm_tools()`` wraps ``_prepare_execution_tools`` so each user tool's
|
|
13
|
+
func is replaced by a shim that dispatches the tool lifecycle to the active
|
|
14
|
+
``CmpndCallback``\\ (s) around the original func call. Routing through the
|
|
15
|
+
callback (rather than through ``Tool.__call__``) keeps execution identical to
|
|
16
|
+
today's bare-func path — no re-validation / pydantic coercion of the
|
|
17
|
+
interpreter's native kwargs — while still emitting the span.
|
|
18
|
+
|
|
19
|
+
``PythonInterpreter.execute`` dispatches tool calls inline on the calling
|
|
20
|
+
(forward) thread, so the shim runs with cmpnd's ``_span_stack`` contextvar
|
|
21
|
+
intact: the tool span nests under ``RLM.forward`` and any ``dspy.Predict`` run
|
|
22
|
+
inside the tool re-parents under it.
|
|
23
|
+
|
|
24
|
+
Signature preservation is mandatory: ``PythonInterpreter._extract_parameters``
|
|
25
|
+
builds its sandbox tool stub from ``inspect.signature(fn)``. ``functools.wraps``
|
|
26
|
+
sets ``__wrapped__``, which ``inspect.signature`` follows, so the real params
|
|
27
|
+
survive the wrap (guarded by a test).
|
|
28
|
+
|
|
29
|
+
Scope is user tools only; the ``llm_query`` / ``llm_query_batched`` builtins
|
|
30
|
+
share the gap but ``llm_query_batched`` fans out over its own thread pool (a
|
|
31
|
+
separate context-propagation problem), so they're left out here.
|
|
32
|
+
|
|
33
|
+
Installed lazily from ``cmpnd.auto_instrument()`` alongside ``patch_gepa()`` /
|
|
34
|
+
``patch_program_state()``. Idempotent; a no-op when dspy isn't installed.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import functools
|
|
40
|
+
import uuid
|
|
41
|
+
from typing import Any, Callable
|
|
42
|
+
|
|
43
|
+
_patched = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _active_cmpnd_callback() -> Any | None:
|
|
47
|
+
"""The single registered CmpndCallback (or None).
|
|
48
|
+
|
|
49
|
+
cmpnd is built around exactly one callback — auto_instrument() refuses to
|
|
50
|
+
add a second — and the callbacks share one span-stack contextvar. So we
|
|
51
|
+
dispatch the tool lifecycle to just the first CmpndCallback: pushing a span
|
|
52
|
+
per callback would nest one tool span under another for a single tool call
|
|
53
|
+
and desync on_tool_end's pop from the stack.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
import dspy
|
|
57
|
+
except ImportError:
|
|
58
|
+
return None
|
|
59
|
+
from . import callback
|
|
60
|
+
|
|
61
|
+
registered = dspy.settings.get("callbacks", []) or []
|
|
62
|
+
for cb in registered:
|
|
63
|
+
if isinstance(cb, callback.CmpndCallback):
|
|
64
|
+
return cb
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _wrap_tool_func(tool_obj: Any) -> Callable[..., Any]:
|
|
69
|
+
"""Wrap a dspy.Tool's func so a call emits a TOOL span around it.
|
|
70
|
+
|
|
71
|
+
The wrapper carries the original func's signature (``functools.wraps`` sets
|
|
72
|
+
``__wrapped__``) so the interpreter's sandbox stub sees the real params. It
|
|
73
|
+
dispatches to the active cmpnd callback only; with none registered it's a
|
|
74
|
+
transparent passthrough.
|
|
75
|
+
"""
|
|
76
|
+
func = tool_obj.func
|
|
77
|
+
|
|
78
|
+
@functools.wraps(func)
|
|
79
|
+
def shim(**kwargs: Any) -> Any:
|
|
80
|
+
cb = _active_cmpnd_callback()
|
|
81
|
+
if cb is None:
|
|
82
|
+
return func(**kwargs)
|
|
83
|
+
|
|
84
|
+
# on_tool_start (which creates + pushes the span) must succeed before we
|
|
85
|
+
# enter the try; on_tool_end is a no-op for an unknown call_id, so a
|
|
86
|
+
# failed start can't leave the span stack unbalanced.
|
|
87
|
+
call_id = uuid.uuid4().hex
|
|
88
|
+
cb.on_tool_start(call_id, tool_obj, kwargs)
|
|
89
|
+
output = None
|
|
90
|
+
exception = None
|
|
91
|
+
try:
|
|
92
|
+
output = func(**kwargs)
|
|
93
|
+
return output
|
|
94
|
+
except Exception as exc: # noqa: BLE001 - re-raised after closing the span
|
|
95
|
+
exception = exc
|
|
96
|
+
raise
|
|
97
|
+
finally:
|
|
98
|
+
cb.on_tool_end(call_id, output, exception)
|
|
99
|
+
|
|
100
|
+
return shim
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def patch_rlm_tools() -> None:
|
|
104
|
+
"""Wrap ``RLM._prepare_execution_tools`` so user tools emit spans.
|
|
105
|
+
|
|
106
|
+
Idempotent (a module-level flag guards double-patching); a no-op when dspy
|
|
107
|
+
or ``dspy.RLM`` isn't available.
|
|
108
|
+
"""
|
|
109
|
+
global _patched
|
|
110
|
+
if _patched:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
import dspy
|
|
115
|
+
except ImportError:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
rlm_cls = getattr(dspy, "RLM", None)
|
|
119
|
+
if rlm_cls is None or not hasattr(rlm_cls, "_prepare_execution_tools"):
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
orig = getattr(rlm_cls, "_prepare_execution_tools")
|
|
123
|
+
|
|
124
|
+
@functools.wraps(orig)
|
|
125
|
+
def _prepare_execution_tools(self: Any) -> dict[str, Callable[..., Any]]:
|
|
126
|
+
tools = orig(self)
|
|
127
|
+
user_tools = getattr(self, "_user_tools", {}) or {}
|
|
128
|
+
for name, tool_obj in user_tools.items():
|
|
129
|
+
if name in tools:
|
|
130
|
+
tools[name] = _wrap_tool_func(tool_obj)
|
|
131
|
+
return tools
|
|
132
|
+
|
|
133
|
+
setattr(rlm_cls, "_prepare_execution_tools", _prepare_execution_tools)
|
|
134
|
+
_patched = True
|
|
@@ -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.4.
|
|
8
|
+
version = "0.4.2"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -236,6 +236,19 @@ _RLM_FIXTURE = (
|
|
|
236
236
|
pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "rlm_district_counter.json"
|
|
237
237
|
)
|
|
238
238
|
|
|
239
|
+
# Two flat-replay fixtures (CMP-187), shared with the local seeder
|
|
240
|
+
# (serve/scripts/seed_fixtures/traces.py make_source_categorizer_trace /
|
|
241
|
+
# make_tool_kind_demo_trace). Unlike _RLM_FIXTURE (pre-digested turns), these
|
|
242
|
+
# are a flat list of spans carrying lid/parent/start_ns + ingest fields, so
|
|
243
|
+
# _replay_flat_fixture just remaps ids + offsets timings. Loaded lazily so a
|
|
244
|
+
# missing file degrades to skipping that seed.
|
|
245
|
+
_SOURCE_CATEGORIZER_FIXTURE = (
|
|
246
|
+
pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "rlm_source_categorizer.json"
|
|
247
|
+
)
|
|
248
|
+
_TOOL_KIND_DEMO_FIXTURE = (
|
|
249
|
+
pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "tool_kind_demo.json"
|
|
250
|
+
)
|
|
251
|
+
|
|
239
252
|
|
|
240
253
|
def _notebook_trace(now: datetime.datetime) -> dict:
|
|
241
254
|
"""The captured "DistrictCounter" dspy.RLM run as a review-app seed trace
|
|
@@ -378,6 +391,75 @@ def _notebook_trace(now: datetime.datetime) -> dict:
|
|
|
378
391
|
}
|
|
379
392
|
|
|
380
393
|
|
|
394
|
+
def _replay_flat_fixture(
|
|
395
|
+
fixture_path: pathlib.Path,
|
|
396
|
+
now: datetime.datetime,
|
|
397
|
+
program_name: str,
|
|
398
|
+
project_tags: list[str],
|
|
399
|
+
request_preview: str,
|
|
400
|
+
response_preview: str,
|
|
401
|
+
) -> dict:
|
|
402
|
+
"""Replay a flat-format fixture (list of spans with lid/parent/start_ns/
|
|
403
|
+
duration_ns + ingest fields) as a review-app seed trace. Mirrors
|
|
404
|
+
make_source_categorizer_trace / make_tool_kind_demo_trace in the local
|
|
405
|
+
seeder, parameterized so both CMP-187 fixtures share one path. Mints a
|
|
406
|
+
fresh trace_id, maps every local id to a fresh uuid (parent_span_id via
|
|
407
|
+
that map, null for the root), offsets each span's start_ns by `now` with
|
|
408
|
+
the same at() helper the other seeds use, and copies the remaining fields
|
|
409
|
+
verbatim into the ingest span shape. Returns the same trace-payload dict
|
|
410
|
+
shape as _notebook_trace.
|
|
411
|
+
"""
|
|
412
|
+
fx = json.loads(fixture_path.read_text())
|
|
413
|
+
root = fx[0]
|
|
414
|
+
trace_id = str(uuid.uuid4())
|
|
415
|
+
total_ns = root["duration_ns"]
|
|
416
|
+
|
|
417
|
+
def at(ns: int) -> str:
|
|
418
|
+
return (now + datetime.timedelta(microseconds=ns / 1000)).isoformat()
|
|
419
|
+
|
|
420
|
+
id_map = {s["lid"]: str(uuid.uuid4()) for s in fx}
|
|
421
|
+
skip_keys = {"lid", "parent", "start_ns", "signature_summary"}
|
|
422
|
+
|
|
423
|
+
spans: list[dict] = []
|
|
424
|
+
total_tokens = 0
|
|
425
|
+
for s in fx:
|
|
426
|
+
parent_lid = s["parent"]
|
|
427
|
+
span: dict = {
|
|
428
|
+
"span_id": id_map[s["lid"]],
|
|
429
|
+
"trace_id": trace_id,
|
|
430
|
+
"parent_span_id": id_map[parent_lid] if parent_lid is not None else None,
|
|
431
|
+
"start_time": at(s["start_ns"]),
|
|
432
|
+
"end_time": at(s["start_ns"] + s["duration_ns"]),
|
|
433
|
+
}
|
|
434
|
+
for k, v in s.items():
|
|
435
|
+
if k not in skip_keys:
|
|
436
|
+
span[k] = v
|
|
437
|
+
if s["span_type"] == "lm_call":
|
|
438
|
+
total_tokens += s.get("total_tokens") or 0
|
|
439
|
+
spans.append(span)
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
"trace_id": trace_id,
|
|
443
|
+
"start_time": at(0),
|
|
444
|
+
"end_time": at(total_ns),
|
|
445
|
+
"duration_ns": total_ns,
|
|
446
|
+
"status": "ok",
|
|
447
|
+
"program_name": program_name, # DSPy module class → programs.module_class → sidebar badge
|
|
448
|
+
"program_version": "0.1.0",
|
|
449
|
+
"program_signature_hash": root["program_signature_hash"],
|
|
450
|
+
"signature_name": root["signature_name"],
|
|
451
|
+
"signature_input_fields": root["signature_input_fields"],
|
|
452
|
+
"signature_output_fields": root["signature_output_fields"],
|
|
453
|
+
"adapter_type": "ChatAdapter",
|
|
454
|
+
"request_preview": request_preview,
|
|
455
|
+
"response_preview": response_preview,
|
|
456
|
+
"project_tags": project_tags,
|
|
457
|
+
"total_tokens": total_tokens,
|
|
458
|
+
"span_count": len(spans),
|
|
459
|
+
"spans": spans,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
381
463
|
def _post(endpoint: str, path: str, payload, api_key: str):
|
|
382
464
|
body = json.dumps(payload).encode("utf-8")
|
|
383
465
|
req = urllib.request.Request(
|
|
@@ -494,6 +576,42 @@ def main() -> int:
|
|
|
494
576
|
# optimization run has none. Unlinked to any optimization run.
|
|
495
577
|
code, _ = _post(endpoint, "/api/v1/traces/batch", [_notebook_trace(now)], api_key)
|
|
496
578
|
log.info("seeded RLM notebook trace (status %s)", code)
|
|
579
|
+
|
|
580
|
+
# 4b) Plant the two CMP-187 tool-span demo traces so every review app
|
|
581
|
+
# shows RLM tool spans (SourceCategorizer, with a vision sub-LLM nested
|
|
582
|
+
# under Tool.inspect_page) and the function-tool vs module-tool
|
|
583
|
+
# differentiation (ToolKindDemo). Each degrades to a skip if its shared
|
|
584
|
+
# fixture is absent, rather than taking the seed down.
|
|
585
|
+
tool_demos: list[dict] = []
|
|
586
|
+
if _SOURCE_CATEGORIZER_FIXTURE.exists():
|
|
587
|
+
tool_demos.append(
|
|
588
|
+
_replay_flat_fixture(
|
|
589
|
+
_SOURCE_CATEGORIZER_FIXTURE,
|
|
590
|
+
now,
|
|
591
|
+
"RLM",
|
|
592
|
+
_SEED_TAGS + ["tool-demo"],
|
|
593
|
+
"vector_pdf source_table/glenn-pdf-p15.pdf (page 1)",
|
|
594
|
+
"candidate_columns, precinct",
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
log.warning("skipping SourceCategorizer seed: fixture missing at %s", _SOURCE_CATEGORIZER_FIXTURE)
|
|
599
|
+
if _TOOL_KIND_DEMO_FIXTURE.exists():
|
|
600
|
+
tool_demos.append(
|
|
601
|
+
_replay_flat_fixture(
|
|
602
|
+
_TOOL_KIND_DEMO_FIXTURE,
|
|
603
|
+
now,
|
|
604
|
+
"ToolKindDemo",
|
|
605
|
+
_SEED_TAGS + ["tool-demo"],
|
|
606
|
+
"espresso place review (function-tool + module-tool)",
|
|
607
|
+
"The speaker loved the new downtown espresso place and its incredible pastries.",
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
log.warning("skipping ToolKindDemo seed: fixture missing at %s", _TOOL_KIND_DEMO_FIXTURE)
|
|
612
|
+
if tool_demos:
|
|
613
|
+
code, _ = _post(endpoint, "/api/v1/traces/batch", tool_demos, api_key)
|
|
614
|
+
log.info("seeded %d CMP-187 tool-demo trace(s) (status %s)", len(tool_demos), code)
|
|
497
615
|
except Exception as exc: # noqa: BLE001 - non-gating; review apps are ephemeral
|
|
498
616
|
log.warning("review seed failed (non-gating): %s", exc)
|
|
499
617
|
|
|
@@ -32,7 +32,7 @@ from tests.e2e import fake_lm
|
|
|
32
32
|
E2E_ENDPOINT = os.getenv("CMPND_E2E_ENDPOINT", "http://localhost:3000")
|
|
33
33
|
CMPND_E2E_API_KEY = os.getenv("CMPND_E2E_API_KEY")
|
|
34
34
|
CMPND_E2E_GROQ_KEY = os.getenv("CMPND_E2E_GROQ_KEY")
|
|
35
|
-
E2E_MODEL = "groq/
|
|
35
|
+
E2E_MODEL = "groq/llama-3.3-70b-versatile"
|
|
36
36
|
|
|
37
37
|
# ---------------------------------------------------------------------------
|
|
38
38
|
# Skip logic — skip entire module when env vars are missing
|
|
@@ -80,7 +80,7 @@ class TestDeployReActWikipedia:
|
|
|
80
80
|
@pytest.fixture(scope="class")
|
|
81
81
|
def deployment(self):
|
|
82
82
|
lm = dspy.LM(
|
|
83
|
-
"groq/
|
|
83
|
+
"groq/llama-3.3-70b-versatile",
|
|
84
84
|
api_key=PLATFORM_GROQ_KEY or "",
|
|
85
85
|
max_tokens=300,
|
|
86
86
|
cache=False,
|
|
@@ -41,7 +41,7 @@ from tests._identity_helpers import program_hash_for_sig
|
|
|
41
41
|
PLATFORM_API_KEY = os.environ.get("PLATFORM_API_KEY")
|
|
42
42
|
PLATFORM_GROQ_KEY = os.environ.get("PLATFORM_GROQ_KEY")
|
|
43
43
|
PLATFORM_ENDPOINT = os.environ.get("PLATFORM_ENDPOINT", "http://localhost:3000")
|
|
44
|
-
STRESS_MODEL = "groq/
|
|
44
|
+
STRESS_MODEL = "groq/llama-3.3-70b-versatile"
|
|
45
45
|
|
|
46
46
|
NUM_THREADS = 5
|
|
47
47
|
ITERATIONS_PER_RUN = 3
|
|
@@ -34,7 +34,7 @@ from tests.stress._reliability import soft_content_check, tolerate_external_outa
|
|
|
34
34
|
PLATFORM_API_KEY = os.environ.get("PLATFORM_API_KEY")
|
|
35
35
|
PLATFORM_GROQ_KEY = os.environ.get("PLATFORM_GROQ_KEY")
|
|
36
36
|
PLATFORM_ENDPOINT = os.environ.get("PLATFORM_ENDPOINT", "http://localhost:3000")
|
|
37
|
-
STRESS_MODEL = "groq/
|
|
37
|
+
STRESS_MODEL = "groq/llama-3.3-70b-versatile"
|
|
38
38
|
|
|
39
39
|
pytestmark = pytest.mark.skipif(
|
|
40
40
|
not PLATFORM_API_KEY or not PLATFORM_GROQ_KEY,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for RLM tool-span instrumentation (_rlm_patch, CMP-187).
|
|
2
|
+
|
|
3
|
+
dspy.RLM hands its code interpreter the bare tool.func, bypassing
|
|
4
|
+
dspy.Tool.__call__'s @with_callbacks, so no tool span is created. The patch
|
|
5
|
+
wraps RLM._prepare_execution_tools so each user tool emits a TOOL span via the
|
|
6
|
+
active cmpnd callback while preserving the func's signature (the interpreter
|
|
7
|
+
builds its sandbox stub from inspect.signature).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
import dspy
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
import cmpnd
|
|
17
|
+
from cmpnd import _rlm_patch, callback, configuration, context, models
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def _configured():
|
|
22
|
+
configuration.configure(api_key="test_key", endpoint="http://localhost:8000")
|
|
23
|
+
yield
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_rlm():
|
|
27
|
+
def add_pages(first: int, last: int) -> int:
|
|
28
|
+
"""Sum a page range."""
|
|
29
|
+
return sum(range(first, last + 1))
|
|
30
|
+
|
|
31
|
+
return dspy.RLM("context, query -> answer", tools=[add_pages]), add_pages
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_patch_wraps_prepare_execution_tools_and_is_idempotent():
|
|
35
|
+
orig = dspy.RLM._prepare_execution_tools
|
|
36
|
+
_rlm_patch._patched = False
|
|
37
|
+
_rlm_patch.patch_rlm_tools()
|
|
38
|
+
wrapped = dspy.RLM._prepare_execution_tools
|
|
39
|
+
assert wrapped is not orig
|
|
40
|
+
_rlm_patch.patch_rlm_tools()
|
|
41
|
+
assert dspy.RLM._prepare_execution_tools is wrapped
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_wrapped_tool_preserves_signature():
|
|
45
|
+
"""The interpreter reads inspect.signature(fn) to build its sandbox stub."""
|
|
46
|
+
_rlm_patch._patched = False
|
|
47
|
+
_rlm_patch.patch_rlm_tools()
|
|
48
|
+
|
|
49
|
+
rlm, add_pages = _make_rlm()
|
|
50
|
+
tools = rlm._prepare_execution_tools()
|
|
51
|
+
shim = tools["add_pages"]
|
|
52
|
+
assert inspect.signature(shim) == inspect.signature(add_pages)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_tool_span_created_and_nested_under_rlm(monkeypatch):
|
|
56
|
+
"""With a callback and an RLM.forward span on the stack, invoking a wrapped
|
|
57
|
+
tool creates a TOOL span parented to the RLM span."""
|
|
58
|
+
_rlm_patch._patched = False
|
|
59
|
+
_rlm_patch.patch_rlm_tools()
|
|
60
|
+
|
|
61
|
+
tracked = []
|
|
62
|
+
cb = callback.CmpndCallback()
|
|
63
|
+
monkeypatch.setattr(cb, "_track_span", lambda span: tracked.append(span))
|
|
64
|
+
monkeypatch.setattr(dspy.settings, "callbacks", [cb], raising=False)
|
|
65
|
+
|
|
66
|
+
context.clear_context()
|
|
67
|
+
context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
|
|
68
|
+
rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
|
|
69
|
+
context.push_span(rlm_span)
|
|
70
|
+
try:
|
|
71
|
+
rlm, _ = _make_rlm()
|
|
72
|
+
tools = rlm._prepare_execution_tools()
|
|
73
|
+
result = tools["add_pages"](first=1, last=3)
|
|
74
|
+
# the tool span is unwound after the tool returns, leaving the RLM span
|
|
75
|
+
span_after_tool = context.get_current_span()
|
|
76
|
+
finally:
|
|
77
|
+
context.pop_span()
|
|
78
|
+
|
|
79
|
+
assert result == 6
|
|
80
|
+
assert span_after_tool is rlm_span
|
|
81
|
+
tool_spans = [s for s in tracked if s.span_type == models.SpanType.TOOL]
|
|
82
|
+
assert len(tool_spans) == 1
|
|
83
|
+
span = tool_spans[0]
|
|
84
|
+
assert span.name == "Tool.add_pages"
|
|
85
|
+
assert span.parent_span_id == rlm_span.span_id
|
|
86
|
+
assert span.trace_id == rlm_span.trace_id
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_multiple_callbacks_emit_one_tool_span(monkeypatch):
|
|
90
|
+
"""Two registered CmpndCallbacks share one span-stack contextvar, so the
|
|
91
|
+
shim dispatches to only the first — one tool span, stack balanced, no
|
|
92
|
+
tool-span-nested-under-tool-span corruption."""
|
|
93
|
+
_rlm_patch._patched = False
|
|
94
|
+
_rlm_patch.patch_rlm_tools()
|
|
95
|
+
|
|
96
|
+
tracked_a, tracked_b = [], []
|
|
97
|
+
cb_a = callback.CmpndCallback()
|
|
98
|
+
cb_b = callback.CmpndCallback()
|
|
99
|
+
monkeypatch.setattr(cb_a, "_track_span", lambda span: tracked_a.append(span))
|
|
100
|
+
monkeypatch.setattr(cb_b, "_track_span", lambda span: tracked_b.append(span))
|
|
101
|
+
monkeypatch.setattr(dspy.settings, "callbacks", [cb_a, cb_b], raising=False)
|
|
102
|
+
|
|
103
|
+
context.clear_context()
|
|
104
|
+
context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
|
|
105
|
+
rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
|
|
106
|
+
context.push_span(rlm_span)
|
|
107
|
+
try:
|
|
108
|
+
rlm, _ = _make_rlm()
|
|
109
|
+
tools = rlm._prepare_execution_tools()
|
|
110
|
+
tools["add_pages"](first=1, last=3)
|
|
111
|
+
span_after_tool = context.get_current_span()
|
|
112
|
+
finally:
|
|
113
|
+
context.pop_span()
|
|
114
|
+
|
|
115
|
+
assert span_after_tool is rlm_span # stack unwound to exactly the RLM span
|
|
116
|
+
tool_spans = [s for s in tracked_a + tracked_b if s.span_type == models.SpanType.TOOL]
|
|
117
|
+
assert len(tool_spans) == 1
|
|
118
|
+
assert tool_spans[0].parent_span_id == rlm_span.span_id
|
|
119
|
+
assert not tracked_b # only the first callback was dispatched to
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_tracing_off_calls_func_without_span():
|
|
123
|
+
"""No cmpnd callback registered -> func still runs, no span emitted."""
|
|
124
|
+
_rlm_patch._patched = False
|
|
125
|
+
_rlm_patch.patch_rlm_tools()
|
|
126
|
+
|
|
127
|
+
context.clear_context()
|
|
128
|
+
original = list(dspy.settings.get("callbacks", []))
|
|
129
|
+
dspy.settings.callbacks = []
|
|
130
|
+
try:
|
|
131
|
+
rlm, _ = _make_rlm()
|
|
132
|
+
tools = rlm._prepare_execution_tools()
|
|
133
|
+
assert tools["add_pages"](first=2, last=4) == 9
|
|
134
|
+
assert context.get_current_span() is None
|
|
135
|
+
finally:
|
|
136
|
+
dspy.settings.callbacks = original
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_raising_tool_closes_span_and_reraises(monkeypatch):
|
|
140
|
+
_rlm_patch._patched = False
|
|
141
|
+
_rlm_patch.patch_rlm_tools()
|
|
142
|
+
|
|
143
|
+
tracked = []
|
|
144
|
+
cb = callback.CmpndCallback()
|
|
145
|
+
monkeypatch.setattr(cb, "_track_span", lambda span: tracked.append(span))
|
|
146
|
+
monkeypatch.setattr(dspy.settings, "callbacks", [cb], raising=False)
|
|
147
|
+
|
|
148
|
+
def boom(x: int) -> int:
|
|
149
|
+
raise ValueError("nope")
|
|
150
|
+
|
|
151
|
+
rlm = dspy.RLM("context, query -> answer", tools=[boom])
|
|
152
|
+
|
|
153
|
+
context.clear_context()
|
|
154
|
+
context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
|
|
155
|
+
rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
|
|
156
|
+
context.push_span(rlm_span)
|
|
157
|
+
try:
|
|
158
|
+
tools = rlm._prepare_execution_tools()
|
|
159
|
+
raised = False
|
|
160
|
+
try:
|
|
161
|
+
tools["boom"](x=1)
|
|
162
|
+
except ValueError:
|
|
163
|
+
raised = True
|
|
164
|
+
assert raised
|
|
165
|
+
# the tool span is closed even when the tool raises
|
|
166
|
+
span_after_tool = context.get_current_span()
|
|
167
|
+
finally:
|
|
168
|
+
context.pop_span()
|
|
169
|
+
|
|
170
|
+
assert span_after_tool is rlm_span
|
|
171
|
+
tool_spans = [s for s in tracked if s.span_type == models.SpanType.TOOL]
|
|
172
|
+
assert len(tool_spans) == 1
|
|
173
|
+
assert tool_spans[0].status == models.TraceStatus.ERROR
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_auto_instrument_installs_rlm_patch():
|
|
177
|
+
_rlm_patch._patched = False
|
|
178
|
+
orig = dspy.RLM._prepare_execution_tools
|
|
179
|
+
cmpnd.auto_instrument()
|
|
180
|
+
assert dspy.RLM._prepare_execution_tools is not orig
|
|
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
|
|
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
|
|
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
|
|
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
|