Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ println!("Average score: {}", score);
```

#### 6. **Optimization** - Optimize your Modules

DSRs provides two powerful optimizers:

**COPRO (Collaborative Prompt Optimization)**
```rust
#[derive(Optimizable)]
pub struct MyModule {
Expand All @@ -212,6 +216,25 @@ let mut module = MyModule::new();
optimizer.compile(&mut module, train_examples).await?;
```

**MIPROv2 (Multi-prompt Instruction Proposal Optimizer v2)** - Advanced optimizer using LLMs
```rust
// MIPROv2 uses a 3-stage process:
// 1. Generate execution traces
// 2. LLM generates candidate prompts with best practices
// 3. Evaluate and select the best prompt

let optimizer = MIPROv2::builder()
.num_candidates(10) // Number of candidate prompts to generate
.num_trials(20) // Number of evaluation trials
.minibatch_size(25) // Examples per evaluation
.temperature(1.0) // Temperature for prompt generation
.build();

optimizer.compile(&mut module, train_examples).await?;
```

See `examples/08-optimize-mipro.rs` for a complete example (requires `parquet` feature).

**Component Freezing:**
```rust
// The Optimizable derive macro automatically implements the trait and marks Module Optimizable
Expand Down Expand Up @@ -315,6 +338,28 @@ struct ComplexReasoningSignature {
}
```

### Optimizer Comparison

| Feature | COPRO | MIPROv2 |
|---------|-------|---------|
| **Approach** | Iterative refinement | LLM-guided generation |
| **Complexity** | Simple | Advanced |
| **Best For** | Quick optimization | Best results |
| **Training Data** | Uses scores | Uses traces & descriptions |
| **Prompting Tips** | No | Yes (15+ best practices) |
| **Program Understanding** | Basic | LLM-generated descriptions |
| **Few-shot Examples** | No | Yes (auto-selected) |

**When to use COPRO:**
- Fast iteration needed
- Simple tasks
- Limited compute budget

**When to use MIPROv2:**
- Best possible results needed
- Complex reasoning tasks
- Have good training data (15+ examples recommended)

---

## 📈 Project Status
Expand Down Expand Up @@ -357,6 +402,7 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS
- Inspired by the original [DSPy](https://github.com/stanfordnlp/dspy) framework
- Built with the amazing Rust ecosystem
- Special thanks to the DSPy community for the discussion and ideas
- MIPROv2 implementation

## 🔗 Resources

Expand Down
177 changes: 177 additions & 0 deletions crates/dspy-rs/examples/08-optimize-mipro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
Example: Optimize a QA module using MIPROv2

This example demonstrates the advanced MIPROv2 optimizer, which uses a 3-stage process:
1. Generate traces from your training data
2. Use an LLM to generate candidate prompts with best practices
3. Evaluate candidates and select the best one

MIPROv2 is more sophisticated than COPRO and typically produces better results
by leveraging prompting best practices and program understanding.

Run with:
```
cargo run --example 08-optimize-mipro --features parquet
```

Note: The `parquet` feature is required for loading HuggingFace datasets.
*/

use anyhow::Result;
use bon::Builder;
use dspy_rs::{
MIPROv2, ChatAdapter, DataLoader, Evaluator, Example, LM, Module, Optimizable, Optimizer,
Predict, Prediction, Predictor, Signature, configure, example,
};
use secrecy::SecretString;

#[Signature]
struct QuestionAnswering {
/// Answer the question accurately and concisely.

#[input]
pub question: String,

#[output]
pub answer: String,
}

#[derive(Builder, Optimizable)]
pub struct SimpleQA {
#[parameter]
#[builder(default = Predict::new(QuestionAnswering::new()))]
pub answerer: Predict,
}

impl Module for SimpleQA {
async fn forward(&self, inputs: Example) -> Result<Prediction> {
self.answerer.forward(inputs).await
}
}

impl Evaluator for SimpleQA {
async fn metric(&self, example: &Example, prediction: &Prediction) -> f32 {
let expected = example
.data
.get("answer")
.and_then(|v| v.as_str())
.unwrap_or("");
let predicted = prediction
.data
.get("answer")
.and_then(|v| v.as_str())
.unwrap_or("");

// Normalize and compare
let expected_normalized = expected.to_lowercase().trim().to_string();
let predicted_normalized = predicted.to_lowercase().trim().to_string();

if expected_normalized == predicted_normalized {
1.0
} else {
// Partial credit for substring matches
if expected_normalized.contains(&predicted_normalized)
|| predicted_normalized.contains(&expected_normalized)
{
0.5
} else {
0.0
}
}
}
}

#[tokio::main]
async fn main() -> Result<()> {
println!("=== MIPROv2 Optimizer Example ===\n");

// Configure the LM
configure(
LM::builder()
.api_key(SecretString::from(std::env::var("OPENAI_API_KEY")?))
.build(),
ChatAdapter {},
);

// Load training data from HuggingFace
println!("Loading training data from HuggingFace...");
let train_examples = DataLoader::load_hf(
"hotpotqa/hotpot_qa",
vec!["question".to_string()],
vec!["answer".to_string()],
"fullwiki",
"validation",
true,
)?;

// Use a small subset for faster optimization
let train_subset = train_examples[..15].to_vec();
println!("Using {} training examples\n", train_subset.len());

// Create the module
let mut qa_module = SimpleQA::builder().build();

// Show initial instruction
println!("Initial instruction:");
println!(
" \"{}\"\n",
qa_module.answerer.get_signature().instruction()
);

// Test baseline performance
println!("Evaluating baseline performance...");
let baseline_score = qa_module.evaluate(train_subset[..5].to_vec()).await;
println!("Baseline score: {:.3}\n", baseline_score);

// Create MIPROv2 optimizer
let optimizer = MIPROv2::builder()
.num_candidates(8) // Generate 8 candidate prompts
.num_trials(15) // Run 15 evaluation trials
.minibatch_size(10) // Evaluate on 10 examples per candidate
.temperature(1.0) // Temperature for prompt generation
.track_stats(true) // Display detailed statistics
.build();

// Optimize the module
println!("Starting MIPROv2 optimization...");
println!("This will:");
println!(" 1. Generate execution traces");
println!(" 2. Create a program description using LLM");
println!(" 3. Generate {} candidate prompts with best practices", 8);
println!(" 4. Evaluate each candidate");
println!(" 5. Select and apply the best prompt\n");

optimizer.compile(&mut qa_module, train_subset.clone()).await?;

// Show optimized instruction
println!("\nOptimized instruction:");
println!(
" \"{}\"\n",
qa_module.answerer.get_signature().instruction()
);

// Test optimized performance
println!("Evaluating optimized performance...");
let optimized_score = qa_module.evaluate(train_subset[..5].to_vec()).await;
println!("Optimized score: {:.3}", optimized_score);

// Show improvement
let improvement = ((optimized_score - baseline_score) / baseline_score) * 100.0;
println!(
"\n✓ Improvement: {:.1}% ({:.3} -> {:.3})",
improvement, baseline_score, optimized_score
);

// Test on a new example
println!("\n--- Testing on a new example ---");
let test_example = example! {
"question": "input" => "What is the capital of France?",
};

let result = qa_module.forward(test_example).await?;
println!("Question: What is the capital of France?");
println!("Answer: {}", result.get("answer", None));

println!("\n=== Example Complete ===");
Ok(())
}
Loading