Metadata-Version: 2.5
Name: cmpnd
Version: 0.17.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.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: 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; 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: mcp<2,>=1.9; extra == 'mcp'
Description-Content-Type: text/markdown

# cmpnd

DSPy observability and deployment SDK for cmpnd.

Automatically trace your DSPy programs with one line of code, and deploy them with one more.

## Installation

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

## Quick Start

```python
import cmpnd

# Configure with your API key
cmpnd.configure(api_key="ck_xxx", project="my-project")

# Enable automatic DSPy instrumentation
cmpnd.auto_instrument()

# Your DSPy code is now automatically traced!
import dspy

lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)

cot = dspy.ChainOfThought("question -> answer")
result = cot(question="What is DSPy?")
# Trace is automatically sent to cmpnd

# Correlate the result with its platform trace (e.g. to attach feedback later):
cmpnd.trace_id(result)  # "a1b2c3d4-..."  (None if not produced under instrumentation)
```

## Configuration

### Using environment variables

```bash
export CMPND_API_KEY="ck_your_api_key"
export CMPND_ENDPOINT="https://platform.cmpnd.ai"  # optional
```

```python
import cmpnd

cmpnd.configure()  # Reads from environment
cmpnd.auto_instrument()
```

### Configuration options

```python
cmpnd.configure(
    api_key="ck_xxx",                # Required: API key
    endpoint="https://platform.cmpnd.ai", # Optional: Backend URL
    project="my-project",            # Optional: Project name
    batch_size=100,                  # Optional: Batch size for export
    flush_interval_seconds=5.0,      # Optional: Flush interval
    capture_inputs=True,             # Optional: Capture function inputs
    capture_outputs=True,            # Optional: Capture function outputs
    default_tags={"env": "prod"},    # Optional: Tags for all traces
)
```

## Deployment

Deploy a DSPy module to the cmpnd platform for server-side execution:

```python
import cmpnd
import dspy

cmpnd.configure(api_key="ck_xxx")

lm = dspy.LM("groq/llama-3.1-8b-instant", temperature=0.0, max_tokens=16384)
dspy.configure(lm=lm)

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

# The returned program is callable — run it server-side like the local module:
result = deployed(question="What is DSPy?")
print(result.answer)
```

The SDK packages the module's source code into a ZIP, uploads it to the platform, and polls until the server-side parser finishes. Returns a `DeployedProgram` you can **call directly** to execute server-side; `str(deployed)` is its `deployment_id` and `deployed.deployment_id` exposes it explicitly.

### With a metric (for optimization)

```python
def accuracy(example, pred, trace=None):
    return example.answer == pred.answer

deployment_id = cmpnd.deploy(module, metric=accuracy)
```

### Options

```python
cmpnd.deploy(
    module,              # Required: a DSPy module
    metric=None,         # Optional: metric callable for optimization support
    timeout=300,         # Optional: seconds to wait for parsing (default 300)
)
```

### How it works

1. The SDK generates source code from the live module (LM config, signature, types)
2. `POST /api/v1/deployments` creates a deployment and returns a presigned S3 upload URL
3. The source ZIP is uploaded directly to S3
4. The server-side parser extracts the program and LM configuration
5. The SDK polls until status is `ready`, then returns the `DeployedProgram`

The deployed program's LM model string (e.g. `groq/llama-3.1-8b-instant`) and configuration (temperature, max_tokens) are extracted automatically from the source.

## Optimization

Optimize a DSPy program through the platform. `cmpnd.optimize()` mirrors `cmpnd.deploy()`: **it returns a `DeployedProgram` you call directly** — it just also tells you the program's `score`.

```python
import cmpnd
import dspy

cmpnd.configure(api_key="ck_xxx")
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))

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

# gepa needs a 5-arg feedback metric returning a 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",                # the only optimizer this platform runs
    trainset=train,
    valset=val,
    metric=metric,
    reflection_lm=dspy.LM("groq/llama-3.3-70b-versatile", max_tokens=16384),  # required
)

print(best.score)                       # best score found
print(best(topic="late autumn").haiku)  # call it directly — same as deploy()
```

`reflection_lm` is the model that **proposes** each new instruction, so the search is only as good as it is — a strong model, typically not the one being optimized. Hand it the `dspy.LM` you would hand upstream GEPA and its sampling reaches the provider; a bare model name is the same LM with no sampling of its own. Don't put an `api_key`, `api_base` or `base_url` on it: the credential a run spends and the endpoint it is spent at are both supplied by the platform, so one set on the LM would be discarded without a word — it is refused before any network call instead. On a hosted org, both live on the provider credential itself (Account → Model Providers), keyed by the model's protocol — one `openai` credential serves every `openai/*` model.

The metric is checked before any network call: **gepa** needs a 5-arg feedback metric, because it writes each new instruction by reading the textual feedback the metric returns. A metric taking `<5` positional arguments can only return a score, so it is **refused** rather than run — with no feedback the run completes and reports success having learned nothing, which is the failure a warning would have let through. A `*args` metric cannot be checked here and is accepted with a warning.

### A metric that judges on a model of its own

A metric is ordinary code, and it 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 with nothing to declare. One that wants a *different* model — a stronger judge than the student, typically — declares it at the optimize call 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"],            # the LM this run declared under that name
    )
    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-5.6-luna", max_tokens=32000),
    lms={"critic": dspy.LM("openai/gpt-5.6-luna", max_tokens=4000)},
    max_metric_calls=300,
)
```

`lms` is the general surface for anything that is not a predictor of the program and wants its own model, so the metric is only its first user. Three things worth knowing before you reach for it:

- **A name the run did not declare raises at the metric**, listing the ones it did — rather than quietly serving the program's model, which is what happens with no way to declare one and which reads afterwards as a search that converged on a bad candidate.
- **`"default-lm"` is reserved**: it is the program's own model, which comes from the deployment. Declaring it is refused, naming it.
- **A model your org holds no credential for is refused when the run is dispatched**, naming the model — before the run starts, not on its first judge call. As with `reflection_lm`, don't set an `api_key`, `api_base` or `base_url` on these LMs; the platform supplies both.

Adding the parameter does not change what your metric is: the arity check reads five or more positional arguments as a feedback metric, and a metric that omits `lms` never sees it.

**`gepa` is the only optimizer this platform runs.** `bootstrap_fewshot` and `random_search` are retired: `optimize()` raises `ValueError` for them before any network call, and the server refuses them with a 404 naming gepa.

### Reusing an existing deployment

Skip the initial deploy by passing the `deployment_id` of a ready deployment as the starting program:

```python
best = cmpnd.optimize(program, optimizer="gepa",
                      trainset=train, valset=val, metric=metric,
                      reflection_lm="groq/llama-3.3-70b-versatile",
                      deployment_id=deployed.deployment_id)
```

### Options

```python
cmpnd.optimize(
    program,                 # Required: the DSPy module to optimize
    optimizer="gepa",        # Required: "gepa" — the only optimizer this platform runs
    trainset=train,          # Required: training examples
    valset=val,              # Required: validation examples
    metric=metric,           # Required: the evaluation metric (arity-checked)
    config=None,             # Optional: optimizer-specific config (allow-listed server-side)
    deployment_id=None,      # Optional: reuse a ready deployment as the starting program
    reflection_lm=None,      # Required: the reflection LM — a dspy.LM, or a bare model name
    lms=None,                # Optional: name -> dspy.LM, the LMs this run declares (see above)
    max_metric_calls=None,   # Required (or `auto`): the metric-call ceiling for one step
    auto=None,               # Required (or `max_metric_calls`): "light" | "medium" | "heavy"
    max_steps=10,            # Optional: maximum optimize steps
    patience=2,              # Optional: stop after N consecutive non-improving steps
    seed=0,                  # Optional: starting seed; advanced each step
)
```

On failure, `optimize()` raises `cmpnd.OptimizeError` (a 4xx initiating a step — e.g. 409 no-data / mismatch) or `cmpnd.OptimizeFailed` (a step that transitioned to `failed`).

### From the CLI

```bash
cmpnd optimize gepa task.py --reflection-model groq/llama-3.3-70b-versatile
```

The snippet (a `.py` file or `-` for stdin) binds `program`, `trainset`, `valset`, and `metric`; the CLI prints the best deployment id.

## Custom Spans

Add custom spans to trace non-DSPy code:

### Using the decorator

```python
from cmpnd import trace, SpanType

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

### Using 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

The SDK automatically captures:

### Module Execution
- Module type (Predict, ChainOfThought, ReAct, etc.)
- Signature name and instructions
- Input/output field names
- Demo count

### LM Calls
- Model name and provider
- Token usage (prompt, completion, total)
- Request/response content

### Adapters
- Format and parse operations
- Input/output transformations

### Tools
- Tool name and description
- Invocation inputs/outputs

### Evaluations
- Evaluation scores
- Program being evaluated

## Span Types

Available span types for categorization:

- `SpanType.MODULE` - Generic DSPy module
- `SpanType.PREDICT` - Predict module
- `SpanType.CHAIN_OF_THOUGHT` - ChainOfThought module
- `SpanType.REACT` - ReAct agent
- `SpanType.RETRIEVE` - Retrieval operations
- `SpanType.LM_CALL` - Language model calls
- `SpanType.ADAPTER_FORMAT` - Adapter formatting
- `SpanType.ADAPTER_PARSE` - Adapter parsing
- `SpanType.TOOL` - Tool invocations
- `SpanType.EVALUATION` - Evaluation runs

## API Reference

### `cmpnd.configure()`

Initialize the SDK with your API key and options.

### `cmpnd.auto_instrument()`

Automatically register 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()`

Deploy a DSPy module to the cmpnd platform. Returns a callable `DeployedProgram`.

### `cmpnd.optimize()`

Optimize a DSPy program through the platform. Returns the best `DeployedProgram` (call it directly; `.score` is the best score found). Raises `cmpnd.OptimizeError` / `cmpnd.OptimizeFailed` on failure.

### `cmpnd.execute()`

Execute a deployed program by `deployment_id`, returning a `dspy.Prediction`. Usually you just call a `DeployedProgram` instead, which dispatches here.

### `cmpnd.DeployedProgram`

A deployed, callable program returned by `cmpnd.deploy()` and `cmpnd.optimize()`. Call it with the original module's input fields; `str()` and `.deployment_id` give its id, and `.score` is its score when it came from `optimize()` (else `None`).

### `cmpnd.trace()`

Decorator for custom traced functions.

### `cmpnd.start_span()`

Context manager for custom spans.

### `cmpnd.get_current_trace()`

Get the current trace (if any).

### `cmpnd.get_current_span()`

Get the current span (if any).

### `cmpnd.trace_id()`

Return the platform trace id stamped on a prediction returned by an instrumented program, or `None` if it wasn't produced under instrumentation. Use it to correlate a prediction with its trace, e.g. to attach feedback via `POST /api/v1/traces/{cmpnd.trace_id(pred)}/metadata`.

### `cmpnd.flush_exporter()`

Force-publish everything the background exporter has queued, keeping it running. Use this when a mid-run reader needs a just-produced trace to exist server-side before fetching it — unlike `shutdown_exporter()`, the exporter stays alive so later spans still publish. Returns `True` if the drain completed within the timeout, `False` if it timed out or no exporter is running — check it before fetching so you don't read a trace that hasn't published yet.

### `cmpnd.shutdown_exporter()`

Gracefully shutdown the background exporter.

## License

MIT
