> ## Documentation Index
> Fetch the complete documentation index at: https://dsrs.herumbshandilya.com/llms.txt
> Use this file to discover all available pages before exploring further.

# COPRO

> Iterative instruction refinement: config, usage, and when to use it

COPRO (Collaborative Prompt Optimization) iteratively refines instructions through generation and evaluation cycles.

## How it works

COPRO runs a generate-and-evaluate loop:

1. **Generate candidates**: Create `breadth` candidate instructions per predictor
2. **Evaluate**: Test each candidate on your training data
3. **Refine**: Use the best candidate as the seed for the next round
4. **Repeat**: Continue for `depth` rounds

The base instruction always competes in every round, so COPRO never installs a candidate that is worse than what you started with.

## Configuration

```rust theme={null}
let copro = COPRO::builder()
    .breadth(10)              // Candidates per round (must be > 1)
    .depth(3)                 // Number of refinement rounds
    .build();
```

Other fields: `prompt_model` (separate LM for generating candidate instructions), `eval_concurrency` (concurrent LM calls during evaluation, default 16), `track_stats`, and `init_temperature` (currently unused, reserved for candidate diversity control). The full field table is in the [optimizers reference](/docs/components/optimizers#copro).

## Usage example

```rust theme={null}
use anyhow::Result;
use bon::Builder;
use dspy_rs::{
    COPRO, Eval, LM, Module, Predict, PredictError,
    Predicted, Signature, Trace, TypedMetric, configure, init_tracing,
};

#[derive(Signature, Clone, Debug)]
struct QA {
    #[input]
    question: String,

    #[output]
    answer: String,
}

#[derive(Builder)]
struct MyModule {
    #[builder(default = Predict::<QA>::new())]
    predictor: Predict<QA>,
}

dspy_rs::predictors!(MyModule { predictor });

impl Module for MyModule {
    type Input = QAInput;
    type Output = QAOutput;

    async fn forward(&self, inputs: QAInput) -> Result<Predicted<QAOutput>, PredictError> {
        self.predictor.call(inputs).await
    }
}

struct ExactMatchMetric;

impl TypedMetric<(QAInput, QAOutput), MyModule> for ExactMatchMetric {
    async fn evaluate(
        &self,
        example: &(QAInput, QAOutput),
        prediction: &Predicted<QAOutput>,
        _trace: Option<&Trace>,
    ) -> Result<Eval> {
        let expected = example.1.answer.trim().to_lowercase();
        let actual = prediction.answer.trim().to_lowercase();
        Ok(Eval::score((expected == actual) as u8 as f64))
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    init_tracing()?;

    // API key automatically read from OPENAI_API_KEY env var
    configure(
        LM::builder()
            .model("openai:gpt-4o-mini".to_string())
            .build()
            .await?,
    );

    let mut module = MyModule::builder().build();
    let trainset = vec![
        (
            QAInput {
                question: "What is 2+2?".to_string(),
            },
            QAOutput {
                answer: "4".to_string(),
            },
        ),
        (
            QAInput {
                question: "Capital of France?".to_string(),
            },
            QAOutput {
                answer: "Paris".to_string(),
            },
        ),
    ];

    let copro = COPRO::builder()
        .breadth(10)
        .depth(3)
        .build();
    let metric = ExactMatchMetric;

    copro.compile_module(&mut module, &trainset, &metric).await?;

    Ok(())
}
```

The trainset here is a slice of `(QAInput, QAOutput)` tuples, the zero-boilerplate row form. Any row type that implements `ToInput<QAInput>` works, including `#[derive(Example)]` structs carrying gold labels and metric-only fields; see [Data](/docs/components/data). The `predictors!` line names the module's optimizable leaves — without it the module does not satisfy the `Predictors` bound `compile_module` requires.

### Typed data loading

Use the shared data ingress reference: [`DataLoader`](/docs/components/data).

## When to use COPRO

**Best for:**

* Quick iteration cycles
* Simple tasks
* Limited compute budget
* Short turnaround requirements

**Avoid when:**

* You need best possible quality (use MIPROv2 or GEPA)
* Task has complex failure modes (use GEPA)
* You want to leverage prompting best practices (use MIPROv2)

## Comparison with other optimizers

| Optimizer          | Strategy                                                  | Needs feedback? | Cost                                       |
| ------------------ | --------------------------------------------------------- | --------------- | ------------------------------------------ |
| `BootstrapFewShot` | One-shot demo harvesting from a teacher pass              | No              | Low (2 × trainset)                         |
| `COPRO`            | Breadth-first instruction search                          | No              | Low (breadth × depth × trainset)           |
| `SIMBA`            | Minibatch introspective ascent (demos + rules)            | No              | Low (steps × minibatch)                    |
| `GEPA`             | Genetic-Pareto evolution with feedback                    | **Yes**         | Medium-high (iterations × eval)            |
| `MIPROv2`          | Trace-guided candidate generation                         | No              | Medium (candidates × trials × trainset)    |
| `Structural`       | LM-guided graph edits over `ir::Edit` (program lane only) | No              | Medium (examples + iterations × minibatch) |

GEPA is the only optimizer that requires textual feedback from the metric (`Eval::with_feedback`). The others use numerical scores alone. Full configuration tables for all six live in the [optimizers reference](/docs/components/optimizers).

## Configuration details

### Breadth

Number of candidate instructions generated at each round, per predictor. Higher breadth means more exploration but proportionally more LM calls. Must be greater than 1; `compile` errors otherwise.

Recommended: 5-15

### Depth

Number of refinement rounds. Each round builds on the best candidate from the previous one, with diminishing returns beyond about 5.

Recommended: 2-5

### Prompt model

An optional separate LM used to generate candidate instructions. Falls back to the global LM set with `configure` when unset.

### Track stats

A per-round statistics flag. COPRO's `compile_module` returns `()`, so nothing is returned either way; leave it at the default.

## Implementation notes

COPRO is a thin strategy over the shared evaluation engine: each candidate instruction is a name-keyed `Candidate` injected ambiently per rollout through a cached, bounded-concurrency fan-out — the module is never mutated during evaluation — and the final winner is installed once through `OptimizeTarget::install`. Repeated candidates are deduplicated by content hash and served from the rollout cache, so re-evaluating an instruction it has already seen costs nothing. See the [optimizer engine reference](/docs/components/optimizer-engine) for the machinery.

Cost is approximately `breadth × depth × num_predictors × trainset_size` LM calls, minus cache hits.

## Examples

<Card title="COPRO Examples" icon="code" href="https://github.com/krypticmouse/DSRs/tree/main/crates/dspy-rs/examples">
  See examples 02-module-iteration-and-updation.rs and 04-optimize-hotpotqa.rs
</Card>
