Metadata-Version: 2.5
Name: cmpnd
Version: 0.39.1
Summary: DSPy observability and deployment SDK for cmpnd
Project-URL: Homepage, https://cmpnd.ai
Author-email: cmpnd <hello@cmpnd.ai>
License: MIT
Keywords: dspy,llm,observability,tracing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: dspy>=3.1.3
Requires-Dist: httpx>=0.27.0
Requires-Dist: ipython>=8.20
Requires-Dist: openai<2
Requires-Dist: pydantic>=2.0
Requires-Dist: typing-extensions>=4.1.0
Requires-Dist: xxhash>=3.0.0
Provides-Extra: dev
Requires-Dist: basedpyright>=1.39; extra == 'dev'
Requires-Dist: boto3>=1.34; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: jsonschema>=4.0; extra == 'dev'
Requires-Dist: numpy>=1.26; (python_version < '3.11') and extra == 'dev'
Requires-Dist: numpy>=2.3; (python_version >= '3.11') and extra == 'dev'
Requires-Dist: pandas>=2.2; extra == 'dev'
Requires-Dist: pillow>=10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-rerunfailures>=14.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Requires-Dist: twine>=7.0; extra == 'dev'
Provides-Extra: fake-provider
Requires-Dist: cmpnd-fake-provider; (python_version >= '3.11') and extra == 'fake-provider'
Provides-Extra: images
Requires-Dist: pillow>=10.0; extra == 'images'
Provides-Extra: mcp
Requires-Dist: dspy>=3.3.1; extra == 'mcp'
Requires-Dist: mcp>=1.9; extra == 'mcp'
Description-Content-Type: text/markdown

# cmpnd

The `cmpnd` package traces DSPy programs and deploys and optimizes them on the
cmpnd platform. One call turns on tracing, and one more deploys a program.

## Installation

```bash
uv add cmpnd      # or: pip install cmpnd
```

## Quick start

```python
import cmpnd
import dspy

cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
cmpnd.auto_instrument()

dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))

cot = dspy.ChainOfThought("question -> answer")
result = cot(question="What is DSPy?")   # the trace goes to cmpnd

cmpnd.trace_id(result)  # "a1b2c3d4-...", or None outside instrumentation
```

`cmpnd.trace_id()` links a result to its platform trace, for example to attach
feedback later.

## Configuration

`cmpnd.configure()` reads `CMPND_API_KEY` and `CMPND_ENDPOINT` when you pass
no arguments. The endpoint defaults to `https://platform.cmpnd.ai`.

```bash
export CMPND_API_KEY="ck_your_api_key"
export CMPND_ENDPOINT="https://platform.cmpnd.ai"  # optional
export CMPND_PROGRAM_ID="p_0123456789Ab"            # optional: the program these traces belong to
```

```python
cmpnd.configure(
    api_key="ck_xxx",                      # required, or CMPND_API_KEY
    endpoint="https://platform.cmpnd.ai",  # optional, or CMPND_ENDPOINT
    project_tags="my-project",             # optional: one tag or a list of tags
    program_id="p_0123456789Ab",           # optional, or CMPND_PROGRAM_ID: the id `cmpnd programs create` prints
    batch_size=100,                        # optional: spans per export batch
    flush_interval_seconds=5.0,            # optional: seconds between flushes
    capture_inputs=True,                   # optional: record span inputs
    capture_outputs=True,                  # optional: record span outputs
    default_tags={"env": "prod"},          # optional: tags on every trace
)
```

`project` is a deprecated alias for `project_tags`. Passing both raises
`ValueError`. `CMPND_MAX_INPUT_SIZE` and `CMPND_MAX_OUTPUT_SIZE` set the
captured input and output size limits, 65,536 bytes each by default.

## Deployment

Every deployment belongs to a program, and the platform issues each program
an id. Create a program first. The command prints its id:

```bash
cmpnd programs create --name qa
```

Then deploy a DSPy module under that program for server-side execution:

```python
import cmpnd
import dspy

cmpnd.configure(api_key="ck_xxx", program_id="p_0123456789Ab")
dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))

module = dspy.Predict("question -> answer")
deployed = cmpnd.deploy(module)

result = deployed(question="What is DSPy?")  # runs server-side
print(result.answer)
```

`deploy()` packages the module's source into a ZIP, uploads it and polls until
the server-side parser finishes. It then reads the deployment's persisted
interface and returns a `DeployedProgram`. Call the program to execute it
server-side. `str(deployed)` and `deployed.deployment_id` return the
deployment ID. `deployed.artifact_metadata` holds the typed artifact record
when the platform has inspected it. An unknown interface leaves `input_fields`
and `output_fields` as `None`. A known interface with no fields returns empty
lists.

`cmpnd.deploy(module, program_id="p_…")` names the program for one call.
Without a configured or passed id, `deploy` raises `ValueError` before it
packages or sends anything.

To deploy a source file, run `cmpnd deploy program.py --program-id p_…`, or
set `CMPND_PROGRAM_ID`. The CLI uploads the file's exact bytes and does not
reconstruct the source from a live object. Use `--source-root` and repeated
`--include` flags for sibling modules or data, and `cmpnd inspect <id>
--source` to read the deployed entry file back. [`docs/cli.md`](docs/cli.md)
covers the CLI.

### Options

```python
cmpnd.deploy(
    module,            # required: a DSPy module
    metric=None,       # optional: a metric callable, packaged for optimization
    timeout=300,       # optional: seconds to wait for the parser after the upload
    metadata=None,     # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
    program_id="p_…",  # required unless configured: the program the deployment belongs to
)
```

### How it works

1. The SDK generates source from the live module, including its LM
   configuration, signature and types.
2. `POST /api/v1/deployments` creates the deployment and returns an upload
   URL.
3. The SDK uploads the source ZIP to that URL. The bytes go to the worker.
4. The worker's parser extracts the program and its LM configuration.
5. The SDK polls until the status is `ready`, reads the artifact metadata and
   returns the `DeployedProgram`.

The parser reads the model string, such as `openai/gpt-6-luna`, and its
sampling settings from the source.

## Optimization

`cmpnd.optimize()` optimizes a program through the platform. Like `deploy()`,
it returns a `DeployedProgram` that you call directly, and the program's
`score` is the best score found.

```python
import cmpnd
import dspy

cmpnd.configure(api_key="ck_xxx", program_id="p_0123456789Ab")  # from `cmpnd programs create`
dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))

program = dspy.ChainOfThought("topic -> haiku")

# GEPA needs a five-argument feedback metric that returns dspy.Prediction(score=, feedback=)
def metric(example, pred, trace=None, pred_name=None, pred_trace=None):
    ok = example.season.lower() not in pred.haiku.lower()
    return dspy.Prediction(score=float(ok),
                           feedback=None if ok else "Don't name the season.")

best = cmpnd.optimize(
    program,
    optimizer="gepa",
    trainset=train,
    valset=val,
    metric=metric,
    reflection_lm=dspy.LM("openai/gpt-6-luna"),
    auto="light",
)

print(best.score)
print(best(topic="late autumn").haiku)
```

`reflection_lm` is the model that proposes each new instruction. The search is
only as good as this model, so it is usually a strong model and not the one
being optimized. A `dspy.LM` passes its sampling settings to the provider. A
bare model name gives the same model with no sampling settings. The platform
supplies the credential and the endpoint, so the SDK refuses an `api_key`,
`api_base` or `base_url` on the LM before any network call. On a hosted org,
both live on the provider credential (Account → Model Providers), keyed by the
model's protocol. One `openai` credential serves every `openai/*` model.

The SDK checks the metric before any network call. GEPA writes each new
instruction from the textual feedback the metric returns, so it needs a
five-argument feedback metric. The SDK refuses a metric with fewer than five
positional arguments, because such a run would report success and learn
nothing. A `*args` metric cannot be checked, and the SDK accepts it with a
warning.

`gepa` is the only optimizer the platform runs. `optimize()` raises
`ValueError` for the retired `bootstrap_fewshot` and `random_search` before
any network call, and serve answers them with a 404 that names gepa.

### A metric with its own model

A metric reaches a model the same way the program's predictors do. A metric
that builds a `dspy.Predict` runs on the program's own model. A metric that
needs a different model, such as a stronger judge, declares it in the
optimize call's `lms` argument and selects it by name through a sixth
parameter, `lms`:

```python
def faithfulness(gold, pred, trace, pred_name, pred_trace, lms=None):
    """Does the summary say only what the document says?"""
    verdict = dspy.Predict("document, summary -> score: float, feedback: str")(
        document=gold.document,
        summary=pred.summary,
        lm=lms["critic"],
    )
    return dspy.Prediction(score=verdict.score, feedback=verdict.feedback)


best = cmpnd.optimize(
    program, optimizer="gepa", trainset=train, valset=val, metric=faithfulness,
    reflection_lm=dspy.LM("openai/gpt-6-luna"),
    lms={"critic": dspy.LM("openai/gpt-6-luna")},
    max_metric_calls=300,
)
```

`lms` declares any model that is not one of the program's predictors. The
rules:

- A name the run did not declare raises at the metric and lists the declared
  names.
- `"default-lm"` is reserved for the program's own model, which comes from the
  deployment. Declaring it raises an error that names it.
- Serve refuses a model your org holds no credential for when it dispatches
  the run, before the run starts. These LMs, like `reflection_lm`, must not set
  `api_key`, `api_base` or `base_url`.

The `lms` parameter does not change the arity check. Five or more positional
arguments make a feedback metric, and a metric without `lms` never receives
it.

A run can also bring its own instruction proposer through `proposer`.
[`docs/custom-proposer.md`](docs/custom-proposer.md) describes it.

### Reusing a deployment

Pass the `deployment_id` of a ready deployment to skip the initial deploy:

```python
best = cmpnd.optimize(program, optimizer="gepa",
                      trainset=train, valset=val, metric=metric,
                      reflection_lm="openai/gpt-6-luna", auto="light",
                      deployment_id=deployed.deployment_id)
```

### Options

```python
cmpnd.optimize(
    program,                 # required: the DSPy module to optimize
    optimizer="gepa",        # required: the only optimizer the platform runs
    trainset=train,          # required: training examples
    valset=val,              # required: validation examples
    metric=metric,           # required: the metric, arity-checked
    proposer=None,           # optional: the run's own instruction proposer
    config=None,             # optional: optimizer config, allow-listed by serve
    deployment_id=None,      # optional: a ready deployment to start from
    reflection_lm=None,      # required: a dspy.LM or a bare model name
    lms=None,                # optional: name -> dspy.LM, the run's named models
    max_metric_calls=None,   # budget: the metric-call ceiling for one step
    auto=None,               # budget: "light", "medium" or "heavy"
    may_author_code=None,    # optional: False keeps each dspy.Flex at its deployed source
    max_steps=10,            # optional: the most optimize steps
    patience=2,              # optional: stop after this many steps without improvement
    seed=0,                  # optional: the starting seed, advanced each step
    poll_interval=3.0,       # optional: seconds between status polls
    timeout=600.0,           # optional: seconds to wait for each step
)
```

Pass exactly one budget: `max_metric_calls`, `auto` or
`config["max_full_evals"]`. The SDK raises before any network call when the
budget is missing.

`optimize()` raises `cmpnd.OptimizeError` when serve answers a step's
initiation with a 4xx, such as a 409 for missing data or a metric mismatch. It
raises `cmpnd.OptimizeFailed` when a step reaches `failed`.

### From the CLI

```bash
cmpnd optimize gepa task.py --program-id p_0123456789Ab --reflection-model openai/gpt-6-luna --auto light
```

The file, or `-` for stdin, binds `program`, `trainset`, `valset` and
`metric`. The CLI prints the best deployment ID. The CLI deploys the file's
program first, so it needs `--program-id` or `CMPND_PROGRAM_ID`. With
`--deployment-id`, the program id is optional.

## Custom spans

Trace non-DSPy code with the decorator:

```python
from cmpnd import trace, SpanType

@trace(name="fetch_documents", span_type=SpanType.RETRIEVE)
def fetch_documents(query: str) -> list[str]:
    return documents
```

Or with the context manager:

```python
from cmpnd import start_span, SpanType

def run_pipeline(query: str):
    with start_span("vector_search", span_type=SpanType.RETRIEVE) as span:
        span.set_attribute("index", "my-faiss-index")
        docs = search(query)
        span.set_outputs({"doc_count": len(docs)})

    return generate(query, docs)
```

## What gets traced

- Modules: the module type, signature name and instructions, input and output
  field names, and demo count.
- LM calls: the model name and provider, token usage and the request and
  response content.
- Adapters: format and parse operations.
- Tools: the tool name and description and each invocation's inputs and
  outputs.
- Evaluations: scores and the program under evaluation.

## Span types

`SpanType` has these members: `UNKNOWN`, `MODULE`, `PREDICT`,
`CHAIN_OF_THOUGHT`, `REACT`, `RLM`, `RETRIEVE`, `LM_CALL`, `ADAPTER_FORMAT`,
`ADAPTER_PARSE`, `TOOL`, `INTERPRETER_EXECUTE`, `SIGNATURE_VALIDATE`,
`DEMO_SELECT`, `OPTIMIZATION` and `EVALUATION`.

## API reference

### `cmpnd.configure()`

Sets the API key, endpoint and options.

### `cmpnd.auto_instrument()`

Registers the callback with DSPy.

### `cmpnd.CmpndCallback`

The callback class, for manual registration:

```python
import dspy
import cmpnd

cmpnd.configure(api_key="ck_xxx")
dspy.configure(callbacks=[cmpnd.CmpndCallback()])
```

### `cmpnd.deploy()`

Deploys a DSPy module under a program id and returns a callable
`DeployedProgram`. It uses `program_id=`, else the configured id.

### `cmpnd.optimize()`

Optimizes a program through the platform. It returns the best
`DeployedProgram`, whose `.score` is the best score found. It raises
`cmpnd.OptimizeError` or `cmpnd.OptimizeFailed` on failure.

### `cmpnd.execute()`

Submits one durable run by `deployment_id`, waits for the result within the
local timeout and returns a `dspy.Prediction`. A `DeployedProgram` call goes
through this function. The SDK sends `Prefer: wait=5`, shortened to fit the
remaining budget, so a fast result arrives without polling. It never repeats
the submission POST.

`timeout` is the whole submit-and-wait budget in seconds, 30 by default, or
`None` for no local limit. `program_name` and `lm` select a module and model
and travel beside the inputs. `metadata` and `tags` take `dict[str, str]`
values that serve applies to the run's root trace. Serve-owned names such as
`deployment_run_id` raise `TypeError` before anything is sent. The errors:

- A refused request raises `httpx.HTTPStatusError`.
- A lost or failed submission reply raises `RunSubmissionUncertain`.
- A spent budget raises `RunWaitTimeout`, which holds the run ID.
- An answer that expired before it was read raises `RunResultExpired`.

`cmpnd.deployment_run_id(result)` returns the run ID of a result, or of an
exception when serve had assigned a run.

`cmpnd.submit_deployment_run(id, inputs)` returns the accepted handle without
waiting. `cmpnd.list_deployment_runs()`, `get_deployment_run(id)`,
`get_deployment_run_result(id)` and `wait_deployment_run(id)` find, inspect and
resume runs. `list_deployment_runs(deployment_id=...)` limits the list to one
deployment. `RunSubmissionUncertain` names the list command to check before
you send new work.

### `cmpnd.DeployedProgram`

The callable program that `deploy()` and `optimize()` return. Call it with the
deployed artifact's input fields. `str()` and `.deployment_id` return its ID.
`.score` is its score when it came from `optimize()`, and `None` otherwise.
`.deployment_signature` holds the typed deployment response, and
`.artifact_metadata` holds its metadata when available.

### `cmpnd.trace()`

A decorator that traces a function.

### `cmpnd.start_span()`

A context manager that opens a span.

### `cmpnd.get_current_trace()`

Returns the current trace, or `None`.

### `cmpnd.get_current_span()`

Returns the current span, or `None`.

### `cmpnd.trace_id()`

Returns the platform trace ID stamped on a prediction from an instrumented
program, or `None` when the prediction came from outside instrumentation. Use
it to attach feedback through
`POST /api/v1/traces/{cmpnd.trace_id(pred)}/metadata`.

### `cmpnd.flush_exporter()`

Publishes everything the background exporter has queued and leaves the
exporter running. Call it before a reader fetches a trace the program just
produced. It returns `True` when the drain finished within the timeout, 5
seconds by default, and `False` when it timed out or no exporter is running.

### `cmpnd.shutdown_exporter()`

Flushes and stops the background exporter.
