Metadata-Version: 2.5
Name: cmpnd
Version: 0.56.0
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.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: dspy>=3.1.3
Requires-Dist: httpx>=0.27.0
Requires-Dist: ipython>=8.20
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: gepa; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: jsonschema>=4.0; extra == 'dev'
Requires-Dist: litellm; extra == 'dev'
Requires-Dist: litellm<1.102; (python_version >= '3.15') and extra == 'dev'
Requires-Dist: numpy>=2.3; 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; 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 a client deploys, runs and
optimizes named programs.

## Installation

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

## Authentication

The `cmpnd` library works with the cmpnd service at
[platform.cmpnd.ai](https://platform.cmpnd.ai). Create an API key on the
platform under Settings → API keys, and pass it as `api_key=` to
`cmpnd.configure()` or `cmpnd.Client()`. For a self-hosted cmpnd install, also
pass the install's URL as `endpoint=`.

## Quick start

`cmpnd.auto_instrument()` records a trace of every DSPy call in the process.
Your code needs no other change. Use it to sketch a [DSPy program](https://dspy.ai/) and see what it
does.

```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. Traces recorded this way have no program name. The platform
groups them by the structure of the program's modules. The API reference
below lists the other options of `cmpnd.configure()`.

## Program names

When you’re ready to name programs, evolve them, and share them with your team, switch
to `cmpnd.Client`. A client replaces `configure()` and `auto_instrument()` and offers
more features. A client records only the programs you run through it, and each of
those programs has a program name. A client takes most of the same arguments as `configure()`.

A program name is the name your organization gives a program, such as `question-answerer`.
Traces, deployments and optimization runs under one program name go to one
program. The platform makes the program the first time your organization uses
the program name.

`@cmpnd.program(name)` above a `dspy.Module` class gives the class a program
name. Building the class takes its own arguments and a required `lm=`. The
`lm=` argument is the `dspy.LM` that the program runs on:

```python
import cmpnd
import dspy

class Answer(dspy.Signature):
    """Answer the question in one short sentence."""
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

@cmpnd.program("question-answerer")
class QA(dspy.Predict):
    def __init__(self):
        super().__init__(Answer)

qa1 = QA(lm=dspy.LM("openai/gpt-6-luna"))
```

A client runs the program and records its trace under the program name:

```python
gepa = dspy.GEPA(metric=metric, reflection_lm=dspy.LM("openai/gpt-6-luna"), auto="light")

with cmpnd.Client(api_key="ck_xxx", project_tags="my-project") as sdk:
    # the trace will go to the program "question-answerer"
    result = sdk.run_program(qa1, question="What is DSPy?")
    qa2 = sdk.optimize_program(qa1, gepa, trainset=train, valset=val)
```

A client is active from construction. The `with` form flushes the client's
traces and closes it on exit. Without `with`, the client stays active until
you call `close()` or the process exits, and its traces go out at exit:

```python
sdk = cmpnd.Client(api_key="ck_xxx", project_tags="my-project")
sdk.run_program(qa2, question="What is DSPy?")

# optional: flushes the traces now, so a later read sees them
sdk.close()
```

The `cmpnd.Program` constructor gives a program name to a module you already
have, such as a `dspy.Predict`. It makes the same kind of
program as the decorator:

```python
qa3 = cmpnd.Program(
    "question-answerer", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-6-luna")
)
```

- A program name starts with an ASCII letter or digit, then has letters,
  digits, `.`, `_` or `-`, at most 64 characters. It does not start with `p_`.
  A bad name raises `ValueError`. Declaring a program makes no network call.
- `optimize_program` runs the optimizer in your process and returns a copy of
  the program that holds the optimized module. The copy keeps the program name
  and the LM.
- Calling a `cmpnd.Program` directly while `auto_instrument()` is on raises a
  `TypeError` that points to `cmpnd.Client`.

## Deployed programs

Every deployment belongs to a program, and each program has a program name.
Give a DSPy module a program name with `cmpnd.Program`, then deploy it through a
client for server-side execution:

```python
import cmpnd
import dspy

sdk = cmpnd.Client(api_key="ck_xxx")

qa4 = cmpnd.Program(
    "question-answerer", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-6-luna")
)
deployed = sdk.deploy_program(qa4)

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

`deploy_program()` packages the module's source, sends it to the platform and
waits until the platform has read it. It then returns a `DeployedProgram`.
`run_deployment()` runs it server-side on the client's account, and also takes
a deployment ID. A `DeployedProgram` is not callable, because a run needs a
client. `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.

The platform makes the program the first time your organization uses the
program name. Later deploys under the same program name go to the same
program, and so do the traces of a client that runs it.

### Options

```python
sdk.deploy_program(
    program,           # required: a cmpnd.Program
    metric=None,       # optional: a metric callable, packaged for optimization
    timeout=300,       # optional: seconds to wait for the deployment to be ready
    metadata=None,     # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
)
```

### How it works

1. The SDK packages the module's source, including its LM configuration,
   signature and types.
2. The platform reads the program, the model string and the sampling settings
   from that source.
3. `deploy_program()` returns the `DeployedProgram` when the deployment is
   ready.

## Optimization on the platform

`optimize_deployment()` optimizes a deployment through the platform. It returns
the best deployment as a `DeployedProgram`, whose `score` is the best score
found. Deploy the program first:

```python
import cmpnd
import dspy

sdk = cmpnd.Client(api_key="ck_xxx")

program = cmpnd.Program(
    "haiku", module=dspy.ChainOfThought("topic -> haiku"), lm=dspy.LM("openai/gpt-6-luna")
)
deployed = sdk.deploy_program(program)

# 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 = sdk.optimize_deployment(
    deployed,
    optimizer="gepa",
    trainset=train,
    valset=val,
    metric=metric,
    reflection_lm=dspy.LM("openai/gpt-6-luna"),
    auto="light",
)

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

`optimize_deployment()` also takes the ID of a ready deployment. The
optimization belongs to the deployment's program, and the program's model comes
from the deployment.

`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 default and the only optimizer the platform runs.
`optimize_deployment()` raises `ValueError` for the retired `bootstrap_fewshot`
and `random_search` before any network call, and the platform 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 = sdk.optimize_deployment(
    deployed, 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.
- The platform 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.

### Options

```python
sdk.optimize_deployment(
    deployed,                # required: a DeployedProgram or a deployment ID
    optimizer="gepa",        # optional: the only optimizer the platform runs
    trainset=train,          # required: training examples
    valset=val,              # required: validation examples
    metric=metric,           # required: the metric, arity-checked
    config=None,             # optional: optimizer config, allow-listed by the platform
    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 checks
    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_deployment()` raises `cmpnd.OptimizeError` when the platform 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`.

## Command line

The package installs the `cmpnd` command. `cmpnd login` saves your API key for
the command line. `cmpnd -h` lists the commands, and `cmpnd COMMAND -h`
describes one:

```
usage: cmpnd [-h] [--version] [--api-key API_KEY] [--endpoint ENDPOINT]
             {auth,login,logout,org,credentials,tool-servers,optimizations,programs,signatures,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
             ...

CMPND platform CLI — deploy, run, and inspect DSPy programs.

options:
  -h, --help           show this help message and exit
  --version            show program's version number and exit
  --api-key API_KEY    API key (or set CMPND_API_KEY)
  --endpoint ENDPOINT  API endpoint (or set CMPND_ENDPOINT)
```

Deploy a source file under a program name, or optimize the program a file
declares:

```bash
cmpnd deploy program.py question-answerer
cmpnd optimize gepa task.py haiku --reflection-model openai/gpt-6-luna --auto light
```

`cmpnd deploy` uploads the file as written. `cmpnd optimize` reads `program`,
`trainset`, `valset` and `metric` from the file, deploys the program under the
program name and prints the best deployment ID.

## 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 global API key, endpoint and options that `auto_instrument()` uses:

```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
    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
)
```

`api_key` and `endpoint` fall back to the `CMPND_API_KEY` and
`CMPND_ENDPOINT` environment variables. `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.

### `cmpnd.auto_instrument()`

Registers the callback with DSPy, so every DSPy call records a trace.

### `cmpnd.Client()`

Takes most of the same arguments as `cmpnd.configure()` and keeps them apart from the
global config. Use it as a context manager or as a plain object.

- `run_program(program, **inputs)` runs a `cmpnd.Program` and records its trace
  under the program name.
- `optimize_program(program, optimizer, trainset=..., valset=...)` compiles a
  `cmpnd.Program` with a DSPy optimizer in your process, records every
  rollout, and returns a copy of the program that holds the optimized module.
  For `dspy.GEPA`, it also records the optimization run.
- `deploy_program(program)` deploys a `cmpnd.Program` under its program name
  and returns a `DeployedProgram`.
- `run_deployment(deployment, **inputs)` runs a deployment and returns a
  `dspy.Prediction`. The section below describes it.
- `optimize_deployment(deployment, ...)` optimizes a deployment on the platform
  and returns the best `DeployedProgram`, whose `.score` is the best score
  found. It raises `cmpnd.OptimizeError` or `cmpnd.OptimizeFailed` on failure.
- `list_traces(**filters)` and `get_trace(trace_id)` read traces with the
  client's credentials. They flush the client's traces first.
- `close()` flushes the client's traces and closes the client. A closed
  client raises `RuntimeError` on every method.

### `cmpnd.program()`

`@cmpnd.program(name)` above a `dspy.Module` class returns a `cmpnd.Program`
class with the same class name. Building it takes the class's own arguments
and a required `lm=` keyword. The decorator refuses a class whose
constructor already takes `lm`. Give an instance of it to `cmpnd.Program` instead.

### `cmpnd.Program()`

`cmpnd.Program(name, *, module, lm)` gives a DSPy module a program name and
the `dspy.LM` it runs on. `@cmpnd.program(name)` builds the same thing from a
`dspy.Module` class.

### `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.Client.run_deployment()`

Submits one durable run of a `DeployedProgram` or a deployment ID, waits for
the result within the local timeout and returns a `dspy.Prediction`. A fast
result returns at once, and the SDK never submits one run twice. A deployment
of another account answers 404.

`timeout` is the whole submit-and-wait budget in seconds, 30 by default, or
`None` for no local limit. `entrypoint` and `lm` select a module and model and
travel beside the inputs. `metadata` and `tags` take `dict[str, str]` values
that the platform applies to the run's root trace. Names the platform sets,
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`.

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

`sdk.submit_deployment_run(deployment, inputs)` returns the accepted handle
without waiting. `sdk.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)` limits the list to one
deployment. `RunSubmissionUncertain` names the list command to check before you
send new work.

### `cmpnd.DeployedProgram`

The deployment that `deploy_program()` and `optimize_deployment()` return. Run
it with `run_deployment()`. `str()` and `.deployment_id` return its ID.
`.score` is its score when it came from `optimize_deployment()`, 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.
