cmpnd 0.24.0__tar.gz → 0.25.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.24.0 → cmpnd-0.25.0}/PKG-INFO +4 -2
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_assessments.py +25 -4
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/assessments.py +19 -3
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/top.py +65 -56
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/shell.py +38 -32
- cmpnd-0.25.0/cmpnd/cli/snippet.py +52 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/client.py +19 -3
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/mcp_servers.py +112 -19
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/packaging.py +157 -2
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/cli.md +30 -3
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/client.md +8 -4
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/sdk-instrumentation.md +69 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/pyproject.toml +15 -7
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/helpers.py +24 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_assessments_cli.py +19 -2
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_valsets_cli.py +4 -1
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_assessments_api.py +22 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_assessments.py +45 -3
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_main.py +2 -2
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_top.py +137 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_client.py +23 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_mcp_tools.py +264 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_program_source.py +291 -1
- {cmpnd-0.24.0 → cmpnd-0.25.0}/uv.lock +6 -2
- {cmpnd-0.24.0 → cmpnd-0.25.0}/.gitignore +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/AGENTS.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/CLAUDE.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/README.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/callback.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/context.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/helpers.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/models.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/README.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/drain.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_callback.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_context.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_execute.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_identity.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_integration.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_models.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_retry.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.24.0 → cmpnd-0.25.0}/todo.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cmpnd
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: DSPy observability and deployment SDK for cmpnd
|
|
5
5
|
Project-URL: Homepage, https://cmpnd.ai
|
|
6
6
|
Author-email: cmpnd <hello@cmpnd.ai>
|
|
@@ -22,6 +22,7 @@ Requires-Dist: httpx>=0.27.0
|
|
|
22
22
|
Requires-Dist: ipython>=8.20
|
|
23
23
|
Requires-Dist: openai<2
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: typing-extensions>=4.1.0
|
|
25
26
|
Requires-Dist: xxhash>=3.0.0
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: basedpyright>=1.39; extra == 'dev'
|
|
@@ -41,7 +42,8 @@ Requires-Dist: cmpnd-fake-provider; (python_version >= '3.11') and extra == 'fak
|
|
|
41
42
|
Provides-Extra: images
|
|
42
43
|
Requires-Dist: pillow>=10.0; extra == 'images'
|
|
43
44
|
Provides-Extra: mcp
|
|
44
|
-
Requires-Dist:
|
|
45
|
+
Requires-Dist: dspy>=3.3.1; extra == 'mcp'
|
|
46
|
+
Requires-Dist: mcp>=1.9; extra == 'mcp'
|
|
45
47
|
Description-Content-Type: text/markdown
|
|
46
48
|
|
|
47
49
|
# cmpnd
|
|
@@ -46,15 +46,31 @@ def create(
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def list(
|
|
49
|
-
program: str
|
|
49
|
+
program: str | None = None,
|
|
50
|
+
*,
|
|
51
|
+
trace_id: str | None = None,
|
|
52
|
+
limit: int = 50,
|
|
53
|
+
offset: int = 0,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
endpoint: str | None = None,
|
|
50
56
|
) -> dict[str, Any]:
|
|
51
|
-
"""List
|
|
57
|
+
"""List assessments by program or by trace, newest first.
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
Pass exactly one of ``program`` (every assessment of that program) or
|
|
60
|
+
``trace_id`` (only that trace's assessments). Returns
|
|
61
|
+
``{"assessments": [...], "total": N, "limit", "offset", "has_more"}``.
|
|
54
62
|
``limit`` (max rows) and ``offset`` (skip this many) page the full set — the
|
|
55
63
|
server clamps ``limit`` to at most 200.
|
|
56
64
|
"""
|
|
57
|
-
|
|
65
|
+
if program is not None and trace_id is not None:
|
|
66
|
+
raise ValueError("pass exactly one of program or trace_id")
|
|
67
|
+
if trace_id is not None:
|
|
68
|
+
scope: dict[str, str] = {"trace_id": trace_id}
|
|
69
|
+
elif program is not None:
|
|
70
|
+
scope = {"program": program}
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError("pass exactly one of program or trace_id")
|
|
73
|
+
params = {**scope, "limit": str(limit), "offset": str(offset)}
|
|
58
74
|
return _client(api_key, endpoint).get("/api/v1/assessments", params=params)
|
|
59
75
|
|
|
60
76
|
|
|
@@ -63,6 +79,11 @@ def get(assessment_id: str, *, api_key: str | None = None, endpoint: str | None
|
|
|
63
79
|
return _client(api_key, endpoint).get(f"/api/v1/assessments/{assessment_id}")
|
|
64
80
|
|
|
65
81
|
|
|
82
|
+
def delete(assessment_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> None:
|
|
83
|
+
"""Delete one assessment. Raises ``ApiError`` (404) if it does not exist."""
|
|
84
|
+
_client(api_key, endpoint).delete(f"/api/v1/assessments/{assessment_id}")
|
|
85
|
+
|
|
86
|
+
|
|
66
87
|
def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
|
|
67
88
|
cfg = configuration.get_config()
|
|
68
89
|
resolved_key = api_key or (cfg.api_key if cfg else None) or os.getenv("CMPND_API_KEY")
|
|
@@ -44,8 +44,14 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
44
44
|
)
|
|
45
45
|
create.set_defaults(func=handle_create)
|
|
46
46
|
|
|
47
|
-
ls = sub.add_parser("list", help="List
|
|
48
|
-
ls.
|
|
47
|
+
ls = sub.add_parser("list", help="List assessments by program or by trace")
|
|
48
|
+
scope = ls.add_mutually_exclusive_group(required=True)
|
|
49
|
+
scope.add_argument(
|
|
50
|
+
"program",
|
|
51
|
+
nargs="?",
|
|
52
|
+
help="Program signature hash (16-hex, as `cmpnd signatures list` shows)",
|
|
53
|
+
)
|
|
54
|
+
scope.add_argument("--trace", dest="trace_id", metavar="TRACE_ID", help="A single trace's assessments")
|
|
49
55
|
pagination.add_args(ls, with_page_aliases=False, default_limit=50)
|
|
50
56
|
ls.set_defaults(func=handle_list)
|
|
51
57
|
|
|
@@ -53,6 +59,10 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
53
59
|
show.add_argument("id", help="Assessment id")
|
|
54
60
|
show.set_defaults(func=handle_show)
|
|
55
61
|
|
|
62
|
+
delete = sub.add_parser("delete", help="Delete one assessment")
|
|
63
|
+
delete.add_argument("id", help="Assessment id")
|
|
64
|
+
delete.set_defaults(func=handle_delete)
|
|
65
|
+
|
|
56
66
|
|
|
57
67
|
def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
58
68
|
body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
|
|
@@ -64,9 +74,15 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
def handle_list(client: api_client.ApiClient, args: Any) -> Any:
|
|
67
|
-
|
|
77
|
+
scope = {"trace_id": args.trace_id} if args.trace_id else {"program": args.program}
|
|
78
|
+
params = {**scope, **pagination.wire(args, "limit_offset", as_str=True)}
|
|
68
79
|
return client.get("/api/v1/assessments", params=params)
|
|
69
80
|
|
|
70
81
|
|
|
71
82
|
def handle_show(client: api_client.ApiClient, args: Any) -> Any:
|
|
72
83
|
return client.get(f"/api/v1/assessments/{args.id}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def handle_delete(client: api_client.ApiClient, args: Any) -> Any:
|
|
87
|
+
client.delete(f"/api/v1/assessments/{args.id}")
|
|
88
|
+
return {"deleted": args.id}
|
|
@@ -15,7 +15,7 @@ from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
|
|
|
15
15
|
from datetime import datetime, timezone
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
-
from .. import api_client, pagination
|
|
18
|
+
from .. import api_client, pagination, snippet
|
|
19
19
|
from . import signatures
|
|
20
20
|
|
|
21
21
|
# --------------------------------------------------------------------------
|
|
@@ -782,30 +782,35 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
|
|
|
782
782
|
|
|
783
783
|
configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
|
|
784
784
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
program = namespace.get("program")
|
|
792
|
-
if program is None:
|
|
793
|
-
# Fallback: pick the last dspy.Module instance defined.
|
|
785
|
+
# The whole packaging step stays inside the registration, not just the exec:
|
|
786
|
+
# both the annotations DSPy resolves and the class sources the packager
|
|
787
|
+
# inlines are read back out of `sys.modules` after the snippet has run.
|
|
788
|
+
with snippet.as_module("__cmpnd_deploy__", source_label) as module:
|
|
789
|
+
namespace: dict[str, Any] = module.__dict__
|
|
794
790
|
try:
|
|
795
|
-
|
|
796
|
-
except
|
|
797
|
-
raise api_client.CliError("Snippet
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
791
|
+
exec(compile(source, source_label, "exec"), namespace)
|
|
792
|
+
except Exception as exc:
|
|
793
|
+
raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
|
|
794
|
+
|
|
795
|
+
program = namespace.get("program")
|
|
796
|
+
if program is None:
|
|
797
|
+
# Fallback: pick the last dspy.Module instance defined.
|
|
798
|
+
try:
|
|
799
|
+
import dspy
|
|
800
|
+
except ImportError:
|
|
801
|
+
raise api_client.CliError("Snippet did not bind `program`, and dspy isn't importable to scan for one.")
|
|
802
|
+
candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
|
|
803
|
+
if not candidates:
|
|
804
|
+
raise api_client.CliError(
|
|
805
|
+
"Snippet did not bind `program` to a dspy.Module instance. Add e.g. `program = dspy.Predict(...)`."
|
|
806
|
+
)
|
|
807
|
+
program = candidates[-1]
|
|
808
|
+
|
|
809
|
+
metadata = _parse_deploy_metadata(getattr(args, "metadata", None))
|
|
804
810
|
|
|
805
|
-
|
|
811
|
+
print(_c("deploying…", "dim"), file=sys.stderr)
|
|
812
|
+
deployed = deployment.deploy(program, metadata=metadata)
|
|
806
813
|
|
|
807
|
-
print(_c("deploying…", "dim"), file=sys.stderr)
|
|
808
|
-
deployed = deployment.deploy(program, metadata=metadata)
|
|
809
814
|
deployment_id = deployed.deployment_id
|
|
810
815
|
print(deployment_id)
|
|
811
816
|
|
|
@@ -882,42 +887,46 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
882
887
|
|
|
883
888
|
configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
|
|
884
889
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
+
# Registered for the whole run, not just the exec: optimize deploys the
|
|
891
|
+
# program, and packaging reads the snippet's classes back through
|
|
892
|
+
# `sys.modules`. See `cmpnd.cli.snippet`.
|
|
893
|
+
with snippet.as_module("__cmpnd_optimize__", source_label) as module:
|
|
894
|
+
namespace: dict[str, Any] = module.__dict__
|
|
895
|
+
try:
|
|
896
|
+
exec(compile(source, source_label, "exec"), namespace)
|
|
897
|
+
except Exception as exc:
|
|
898
|
+
raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
|
|
890
899
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
900
|
+
missing = [name for name in ("program", "trainset", "valset", "metric") if namespace.get(name) is None]
|
|
901
|
+
if missing:
|
|
902
|
+
raise api_client.CliError(
|
|
903
|
+
f"Snippet must bind {missing} — e.g. `program = dspy.Predict('q -> a')`, "
|
|
904
|
+
"`trainset = [...]`, `valset = [...]`, "
|
|
905
|
+
"`def metric(gold, pred, trace, pred_name, pred_trace): ...` returning "
|
|
906
|
+
"`dspy.Prediction(score=, feedback=)`."
|
|
907
|
+
)
|
|
899
908
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
909
|
+
print(_c(f"optimizing with {args.optimizer}…", "dim"), file=sys.stderr)
|
|
910
|
+
try:
|
|
911
|
+
result = optimization.optimize(
|
|
912
|
+
namespace["program"],
|
|
913
|
+
optimizer=args.optimizer,
|
|
914
|
+
trainset=namespace["trainset"],
|
|
915
|
+
valset=namespace["valset"],
|
|
916
|
+
metric=namespace["metric"],
|
|
917
|
+
deployment_id=getattr(args, "deployment_id", None),
|
|
918
|
+
# The flag is named for what a command line can carry — a model name,
|
|
919
|
+
# never an LM object — and the parameter for what the Python surface
|
|
920
|
+
# accepts. Each layer's name says what that layer takes.
|
|
921
|
+
reflection_lm=getattr(args, "reflection_model", None),
|
|
922
|
+
max_metric_calls=getattr(args, "max_metric_calls", None),
|
|
923
|
+
auto=getattr(args, "auto", None),
|
|
924
|
+
max_steps=getattr(args, "max_steps", 10),
|
|
925
|
+
patience=getattr(args, "patience", 2),
|
|
926
|
+
seed=getattr(args, "seed", 0),
|
|
927
|
+
)
|
|
928
|
+
except (optimization.OptimizeError, optimization.OptimizeFailed, ValueError) as exc:
|
|
929
|
+
raise api_client.CliError(str(exc))
|
|
921
930
|
|
|
922
931
|
if getattr(args, "json", False):
|
|
923
932
|
print(
|
|
@@ -33,6 +33,7 @@ from types import SimpleNamespace
|
|
|
33
33
|
from typing import Any, Callable
|
|
34
34
|
|
|
35
35
|
from . import api_client, commands, credentials, verbs
|
|
36
|
+
from .snippet import as_module
|
|
36
37
|
|
|
37
38
|
# --------------------------------------------------------------------------
|
|
38
39
|
# Config + dismissible-tips marker
|
|
@@ -508,38 +509,43 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
|
|
|
508
509
|
|
|
509
510
|
configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
|
|
510
511
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
512
|
+
# Registered for the whole deploy, not just the exec: the annotations
|
|
513
|
+
# DSPy resolves and the class sources the packager inlines are both read
|
|
514
|
+
# back through `sys.modules`. See `cmpnd.cli.snippet`.
|
|
515
|
+
with as_module("__cmpnd_deploy__", label) as module:
|
|
516
|
+
namespace: dict[str, Any] = module.__dict__
|
|
517
|
+
try:
|
|
518
|
+
exec(compile(snippet, label, "exec"), namespace)
|
|
519
|
+
except SystemExit as exc:
|
|
520
|
+
# IPython catches SystemExit internally; the stdlib code.interact
|
|
521
|
+
# fallback would re-raise and kill the whole REPL. Convert to a
|
|
522
|
+
# plain exception so behavior is uniform across REPL flavors.
|
|
523
|
+
# The snippet runs with __name__ == "__cmpnd_deploy__", so a
|
|
524
|
+
# standard `if __name__ == "__main__":` guard already prevents
|
|
525
|
+
# this — call that out so users know how to fix.
|
|
526
|
+
raise RuntimeError(
|
|
527
|
+
f"Snippet called sys.exit({exc.code!r}). Remove the call or guard it "
|
|
528
|
+
'with `if __name__ == "__main__":` — the deploy runner sets '
|
|
529
|
+
"__name__ to '__cmpnd_deploy__', so a __main__ guard short-circuits "
|
|
530
|
+
"in deploy but still works when the file is run directly."
|
|
531
|
+
) from None
|
|
532
|
+
|
|
533
|
+
program = namespace.get("program")
|
|
534
|
+
if program is None:
|
|
535
|
+
import dspy
|
|
536
|
+
|
|
537
|
+
candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
|
|
538
|
+
if not candidates:
|
|
539
|
+
raise ValueError(
|
|
540
|
+
"Snippet did not bind `program` to a dspy.Module instance "
|
|
541
|
+
"(e.g. `program = dspy.Predict('q -> a')`)."
|
|
542
|
+
)
|
|
543
|
+
program = candidates[-1]
|
|
544
|
+
|
|
545
|
+
sys.stderr.write(_c("deploying…\n", "dim"))
|
|
546
|
+
sys.stderr.flush()
|
|
547
|
+
deployed = deployment.deploy(program)
|
|
548
|
+
|
|
543
549
|
record = client.get(f"/api/v1/deployments/{deployed.deployment_id}")
|
|
544
550
|
deps.refresh() # new deployment becomes visible in the registry
|
|
545
551
|
return Deployment(client, record)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Run a user's deploy/optimize snippet as a real, importable module.
|
|
2
|
+
|
|
3
|
+
A snippet exec'd into a bare dict is not reachable through ``sys.modules``, and
|
|
4
|
+
two things the snippet's own program depends on look it up there:
|
|
5
|
+
|
|
6
|
+
* **Deferred annotations.** With ``from __future__ import annotations`` a field's
|
|
7
|
+
type is stored as the string ``"Genre"``. Whatever resolves it later — DSPy
|
|
8
|
+
rebuilding a signature for ``ChainOfThought``, pydantic rebuilding a model —
|
|
9
|
+
reads the defining module's namespace out of ``sys.modules``. With no module
|
|
10
|
+
there the string stays a ``ForwardRef`` and the deploy is refused with "Field
|
|
11
|
+
types must be types".
|
|
12
|
+
* **Source recovery.** The packager inlines a user-defined annotation type by
|
|
13
|
+
copying its source, and ``inspect`` finds a class's file through
|
|
14
|
+
``sys.modules[cls.__module__].__file__``. Without the module the type silently
|
|
15
|
+
degrades to a stdlib stand-in.
|
|
16
|
+
|
|
17
|
+
So the snippet gets a module object, registered for as long as the command needs
|
|
18
|
+
it and removed afterwards.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import sys
|
|
25
|
+
import types
|
|
26
|
+
from collections.abc import Generator
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextlib.contextmanager
|
|
30
|
+
def as_module(module_name: str, label: str) -> Generator[types.ModuleType]:
|
|
31
|
+
"""Yield a module for a snippet to exec into, registered under ``module_name``.
|
|
32
|
+
|
|
33
|
+
``label`` is the snippet's origin — a path, or ``<stdin>`` — and becomes the
|
|
34
|
+
module's ``__file__``, which is what lets ``inspect`` recover the source of a
|
|
35
|
+
class the snippet declares.
|
|
36
|
+
|
|
37
|
+
The registration is undone on the way out, whether the snippet raised or not,
|
|
38
|
+
so a REPL session or a second command in the same process never inherits the
|
|
39
|
+
previous snippet's module. Anything already registered under the name is put
|
|
40
|
+
back rather than dropped.
|
|
41
|
+
"""
|
|
42
|
+
module = types.ModuleType(module_name)
|
|
43
|
+
module.__file__ = label
|
|
44
|
+
displaced = sys.modules.get(module_name)
|
|
45
|
+
sys.modules[module_name] = module
|
|
46
|
+
try:
|
|
47
|
+
yield module
|
|
48
|
+
finally:
|
|
49
|
+
if displaced is None:
|
|
50
|
+
sys.modules.pop(module_name, None)
|
|
51
|
+
else:
|
|
52
|
+
sys.modules[module_name] = displaced
|
|
@@ -182,11 +182,22 @@ class Client:
|
|
|
182
182
|
endpoint=self._config.endpoint,
|
|
183
183
|
)
|
|
184
184
|
|
|
185
|
-
def list_assessments(
|
|
186
|
-
|
|
185
|
+
def list_assessments(
|
|
186
|
+
self, program: str | None = None, *, trace_id: str | None = None, limit: int = 50, offset: int = 0
|
|
187
|
+
) -> list[dict[str, Any]]:
|
|
188
|
+
"""Assessments newest first (paged by limit/offset).
|
|
189
|
+
|
|
190
|
+
Pass exactly one of ``program`` (all of that program's assessments) or
|
|
191
|
+
``trace_id`` (one trace's assessments).
|
|
192
|
+
"""
|
|
187
193
|
self._check_active()
|
|
188
194
|
page = _assessments.list(
|
|
189
|
-
program,
|
|
195
|
+
program,
|
|
196
|
+
trace_id=trace_id,
|
|
197
|
+
limit=limit,
|
|
198
|
+
offset=offset,
|
|
199
|
+
api_key=self._config.api_key,
|
|
200
|
+
endpoint=self._config.endpoint,
|
|
190
201
|
)
|
|
191
202
|
return page.get("assessments", [])
|
|
192
203
|
|
|
@@ -195,6 +206,11 @@ class Client:
|
|
|
195
206
|
self._check_active()
|
|
196
207
|
return _assessments.get(assessment_id, api_key=self._config.api_key, endpoint=self._config.endpoint)
|
|
197
208
|
|
|
209
|
+
def delete_assessment(self, assessment_id: str) -> None:
|
|
210
|
+
"""Delete one assessment. Raises if it does not exist."""
|
|
211
|
+
self._check_active()
|
|
212
|
+
_assessments.delete(assessment_id, api_key=self._config.api_key, endpoint=self._config.endpoint)
|
|
213
|
+
|
|
198
214
|
def create_valset(self, program: str, *, name: str | None = None) -> dict[str, Any]:
|
|
199
215
|
"""Create a program-owned valset; name auto-assigned when omitted."""
|
|
200
216
|
self._check_active()
|
|
@@ -52,8 +52,8 @@ import atexit
|
|
|
52
52
|
import keyword
|
|
53
53
|
import logging
|
|
54
54
|
import threading
|
|
55
|
-
from collections.abc import Callable, Coroutine, Mapping
|
|
56
|
-
from contextlib import AsyncExitStack
|
|
55
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine, Mapping
|
|
56
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
57
57
|
from dataclasses import dataclass, field
|
|
58
58
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
59
59
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
@@ -530,35 +530,126 @@ class _SessionHost:
|
|
|
530
530
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
531
531
|
|
|
532
532
|
|
|
533
|
+
_MCP_IS_NOT_INSTALLED = (
|
|
534
|
+
"cmpnd.mcp_tools needs the MCP client library, which is an optional extra. "
|
|
535
|
+
"Install it with `pip install 'cmpnd[mcp]'` (or `uv add 'cmpnd[mcp]'`)."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# How long a tool call may take to be answered on the server-sent stream. The stream
|
|
539
|
+
# is meant to idle between messages, and the older client took this as a separate
|
|
540
|
+
# `sse_read_timeout` argument that cmpnd left at its default. The newer client folds
|
|
541
|
+
# every timeout into one httpx client, so cmpnd has to name it to keep `timeout_s`
|
|
542
|
+
# from cutting a long-running tool call short. It is NOT what bounds discovery —
|
|
543
|
+
# `_connect` holds the caller's `timeout_s` over initialize and the tool listing,
|
|
544
|
+
# because a budget that governs only the connect phase is no budget at all.
|
|
545
|
+
_TOOL_CALL_READ_TIMEOUT_S = 300.0
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _installed_mcp_version() -> str:
|
|
549
|
+
"""The installed MCP client library's version, for a message that has to name it."""
|
|
550
|
+
from importlib import metadata
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
return metadata.version("mcp")
|
|
554
|
+
except Exception:
|
|
555
|
+
return "unknown version"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@asynccontextmanager
|
|
559
|
+
async def _streamable_http_streams(
|
|
560
|
+
url: str, headers: Mapping[str, str], timeout_s: float
|
|
561
|
+
) -> AsyncGenerator[tuple[Any, Any]]:
|
|
562
|
+
"""Yield the read and write streams of a streamable-HTTP session to `url`.
|
|
563
|
+
|
|
564
|
+
Two generations of the MCP client library reach the same transport by different
|
|
565
|
+
shapes, and this is where the difference stops: `streamable_http_client` takes a
|
|
566
|
+
prebuilt httpx client, `streamablehttp_client` takes `headers` and `timeout`, and
|
|
567
|
+
the older one yields a third value (a session-id getter) that nothing here reads.
|
|
568
|
+
Which spelling exists is not a reliable guide to which shape it has — a client
|
|
569
|
+
library carrying both names yields three values from either — so the streams are
|
|
570
|
+
taken by position rather than unpacked.
|
|
571
|
+
|
|
572
|
+
Raises `ImportError` naming what to install when the library is absent, and a
|
|
573
|
+
different one naming the installed version when it is present but exposes neither
|
|
574
|
+
transport.
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
import mcp.client.streamable_http as transport # pyright: ignore[reportMissingImports]
|
|
578
|
+
except ImportError as exc:
|
|
579
|
+
raise ImportError(_MCP_IS_NOT_INSTALLED) from exc
|
|
580
|
+
|
|
581
|
+
with_prebuilt_client = getattr(transport, "streamable_http_client", None)
|
|
582
|
+
if with_prebuilt_client is not None:
|
|
583
|
+
httpx_module = getattr(transport, "httpx2", None) or getattr(transport, "httpx", None)
|
|
584
|
+
timeout: Any = timeout_s
|
|
585
|
+
if httpx_module is not None:
|
|
586
|
+
timeout = httpx_module.Timeout(timeout_s, read=_TOOL_CALL_READ_TIMEOUT_S)
|
|
587
|
+
http_client = transport.create_mcp_http_client(headers=dict(headers), timeout=timeout)
|
|
588
|
+
# A client the transport did not create is a client the transport does not
|
|
589
|
+
# close, so it is closed here — after the session that uses it.
|
|
590
|
+
async with http_client, with_prebuilt_client(url, http_client=http_client) as streams:
|
|
591
|
+
yield streams[0], streams[1]
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
with_headers = getattr(transport, "streamablehttp_client", None)
|
|
595
|
+
if with_headers is None:
|
|
596
|
+
raise ImportError(
|
|
597
|
+
f"cmpnd.mcp_tools cannot drive the installed MCP client library "
|
|
598
|
+
f"({_installed_mcp_version()}): mcp.client.streamable_http exposes neither "
|
|
599
|
+
"`streamable_http_client` nor `streamablehttp_client`, the two streamable-HTTP "
|
|
600
|
+
"transports cmpnd speaks. Install a version that has one of them."
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
async with with_headers(url, headers=dict(headers), timeout=timeout_s) as streams:
|
|
604
|
+
yield streams[0], streams[1]
|
|
605
|
+
|
|
606
|
+
|
|
533
607
|
def _connect(url: str, headers: Mapping[str, str], timeout_s: float) -> tuple[Any, list[Any]]:
|
|
534
608
|
"""Open a session to `url` on the shared host loop and list the server's tools.
|
|
535
609
|
|
|
536
610
|
The one function in this module that dials a network, kept separate so everything
|
|
537
611
|
around it — the refusals, the conversion, the recording — is testable without one.
|
|
612
|
+
|
|
613
|
+
`timeout_s` bounds the whole of that: connecting, the initialize handshake and the
|
|
614
|
+
tool listing. It is held here rather than left to the transport because neither
|
|
615
|
+
client generation bounds those two calls by it — the modern one reads them on a
|
|
616
|
+
stream whose budget is a tool call's, and a caller asking for five seconds is
|
|
617
|
+
asking about the server answering at all, not about one socket read.
|
|
538
618
|
"""
|
|
539
619
|
host = _SessionHost.shared()
|
|
540
620
|
|
|
541
621
|
async def _open() -> tuple[Any, list[Any]]:
|
|
542
622
|
try:
|
|
543
623
|
from mcp import ClientSession # pyright: ignore[reportMissingImports]
|
|
544
|
-
from mcp.client.streamable_http import ( # pyright: ignore[reportMissingImports]
|
|
545
|
-
streamablehttp_client,
|
|
546
|
-
)
|
|
547
624
|
except ImportError as exc:
|
|
548
|
-
raise ImportError(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
625
|
+
raise ImportError(_MCP_IS_NOT_INSTALLED) from exc
|
|
626
|
+
|
|
627
|
+
async def _discover() -> tuple[Any, list[Any], AsyncExitStack]:
|
|
628
|
+
# The stack is opened and, on any failure, closed inside this coroutine:
|
|
629
|
+
# the transport's cancel scopes must be exited by the task that entered
|
|
630
|
+
# them, and on timeout that task is this one rather than its caller.
|
|
631
|
+
stack = AsyncExitStack()
|
|
632
|
+
try:
|
|
633
|
+
read, write = await stack.enter_async_context(_streamable_http_streams(url, headers, timeout_s))
|
|
634
|
+
session = await stack.enter_async_context(ClientSession(read, write))
|
|
635
|
+
await session.initialize()
|
|
636
|
+
listing = await session.list_tools()
|
|
637
|
+
except BaseException:
|
|
638
|
+
await stack.aclose()
|
|
639
|
+
raise
|
|
640
|
+
return session, list(listing.tools), stack
|
|
552
641
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
642
|
+
try:
|
|
643
|
+
session, tools, stack = await asyncio.wait_for(_discover(), timeout_s)
|
|
644
|
+
except asyncio.TimeoutError as exc:
|
|
645
|
+
raise TimeoutError(
|
|
646
|
+
f"the MCP server at {url} did not finish listing its tools within {timeout_s}s. "
|
|
647
|
+
"cmpnd.mcp_tools connects, initializes and reads the listing within the timeout you "
|
|
648
|
+
"pass it, so that a server which never answers costs you that and no more. Raise "
|
|
649
|
+
"timeout_s if the server is simply slow to start."
|
|
650
|
+
) from exc
|
|
559
651
|
host.adopt(stack)
|
|
560
|
-
|
|
561
|
-
return session, list(listing.tools)
|
|
652
|
+
return session, tools
|
|
562
653
|
|
|
563
654
|
return host.run(_open())
|
|
564
655
|
|
|
@@ -586,8 +677,10 @@ def mcp_tools(
|
|
|
586
677
|
Optional: a server with exactly one credential resolves without a name, and
|
|
587
678
|
a server with two makes the deploy name one or be refused. Naming one here
|
|
588
679
|
is also how you rotate a specific key rather than whichever one is there.
|
|
589
|
-
timeout_s: How long to wait
|
|
590
|
-
|
|
680
|
+
timeout_s: How long to wait for the server to answer — connecting,
|
|
681
|
+
initializing and listing its tools — and, for OAuth, on the token
|
|
682
|
+
endpoint. A tool call the returned tools make later is answered on the
|
|
683
|
+
session's own stream, which keeps a longer budget of its own.
|
|
591
684
|
|
|
592
685
|
Returns:
|
|
593
686
|
A list of `dspy.Tool`, each carrying an `McpToolReference` readable with
|