
A high-performance DSPy rewrite in Rust for building LM-powered applications
Documentation • API Reference • Examples • Issues • Discord
DSRs (DSPy Rust) is a ground-up rewrite of the DSPy framework in Rust, designed for building robust, high-performance applications powered by Language Models. Unlike a simple port, DSRs leverages Rust's type system, memory safety, and concurrency features to provide a more efficient and reliable foundation for LM applications.
Add DSRs to your Cargo.toml:
[dependencies]
# Option 1: Use the shorter alias (recommended)dsrs = { package = "dspy-rs", version = "0.7.3" }
# Option 2: Use the full namedspy-rs = "0.7.3"Or use cargo:
# Option 1: Add with alias (recommended)
cargo add dsrs --package dspy-rs
# Option 2: Add with full name
cargo add dspy-rsHere's a simple example to get you started:
use anyhow::Result;use dspy_rs::{configure, init_tracing,LM,Predict,Signature};#[derive(Signature,Clone)]structSentimentAnalyzer{/// Predict the sentiment of the given text 'Positive', 'Negative', or 'Neutral'.#[input]pubtext:String,#[output]pubsentiment:String,}#[tokio::main]asyncfnmain() -> Result<()>{init_tracing()?;// API key automatically read from OPENAI_API_KEY env varconfigure(LM::builder().model("gpt-4o-mini".to_string()).temperature(0.5).build().await?,);// Create a predictorlet predictor = Predict::<SentimentAnalyzer>::new();// Prepare typed inputlet input = SentimentAnalyzerInput{text:"Acme is a great company with excellent customer service.".to_string(),};// Execute predictionlet result = predictor.call(input).await?;println!("Answer: {}", result.sentiment);Ok(())}Result:
Answer: "Positive"
DSRs follows a modular architecture with clear separation of concerns:
dsrs/
├── core/ # Core abstractions (LM, Module, Signature)
├── adapter/ # LM provider adapters (OpenAI, etc.)
├── data/ # Data structures (Example, Prediction)
├── predictors/ # Built-in predictors (Predict, Chain, etc.)
├── evaluate/ # Evaluation framework and metrics
└── macros/ # Derive macros for signatures
#[derive(Signature,Clone)]structTranslationSignature{/// Translate the text accurately while preserving meaning#[input]pubtext:String,#[input]pubtarget_language:String,#[output]pubtranslation:String,}#[derive(Builder, facet::Facet)]#[facet(crate = facet)]pubstructCustomModule{predictor:Predict<TranslationSignature>,}implModuleforCustomModule{typeInput = TranslationSignatureInput;typeOutput = TranslationSignatureOutput;asyncfnforward(&self,input:TranslationSignatureInput) -> Result<Predicted<TranslationSignatureOutput>,PredictError>{self.predictor.call(input).await}}// Get predictionlet predict = Predict::<MySignature>::new();// Configure with OpenAI (API key read from OPENAI_API_KEY env var)let lm = LM::builder().model("gpt-4o-mini".to_string()).temperature(0.7).max_tokens(1000).build().await?;// For local models (e.g., vLLM, Ollama)let lm = LM::builder().base_url("http://localhost:11434".to_string()).model("llama3".to_string()).build().await?;structExactMatchMetric;implTypedMetric<MySignature,MyModule>forExactMatchMetric{asyncfnevaluate(&self,example:&Example<MySignature>,prediction:&Predicted<MySignatureOutput>,) -> Result<MetricOutcome>{let expected = example.output.answer.trim().to_lowercase();let actual = prediction.answer.trim().to_lowercase();Ok(MetricOutcome::score((expected == actual)asu8asf32))}}// Evaluate your modulelet test_examples = load_test_data();let module = MyModule::new();let metric = ExactMatchMetric;// Automatically runs predictions and computes average metriclet outcomes = evaluate_trainset(&module,&test_examples,&metric).await?;let score = average_score(&outcomes);println!("Average score: {}", score);DSRs provides two powerful optimizers:
COPRO (Collaborative Prompt Optimization)
#[derive(Builder, facet::Facet)]#[facet(crate = facet)]pubstructMyModule{predictor:Predict<MySignature>,}// Create and configure the optimizerlet optimizer = COPRO::builder().breadth(10)// Number of candidates per iteration.depth(3)// Number of refinement iterations.build();// Prepare training datalet train_examples = load_training_data();let metric = ExactMatchMetric;// Compile optimizes the module in-placeletmut module = MyModule::new();
optimizer.compile(&mut module, train_examples,&metric).await?;MIPROv2 (Multi-prompt Instruction Proposal Optimizer v2) - Advanced optimizer using LLMs
// MIPROv2 uses a 3-stage process:// 1. Generate execution traces// 2. LLM generates candidate prompts with best practices// 3. Evaluate and select the best promptlet 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,&metric).await?;DataLoader now provides typed loaders that return Vec<Example<S>> directly.
Default behavior is:
- Unknown source fields are ignored.
- Missing signature-required fields return an error with row + field context.
use dspy_rs::{DataLoader,Signature,TypedLoadOptions};#[derive(Signature,Clone,Debug)]structQA{#[input]question:String,#[output]answer:String,}let trainset = DataLoader::load_csv::<QA>("data/train.csv",',',true,TypedLoadOptions::default(),)?;For custom source schemas, use mapper overloads:
let trainset = DataLoader::load_csv_with::<QA,_>("data/train.csv",',',true,TypedLoadOptions::default(),
|row| {Ok(dspy_rs::Example::new(QAInput{question: row.get::<String>("prompt")?,},QAOutput{answer: row.get::<String>("completion")?,},))},)?;Migration note:
- Removed legacy raw signatures that required
input_keys/output_keys. save_json/save_csvwere removed fromDataLoader.- Use typed
load_*/load_*_withAPIs.
See examples/08-optimize-mipro.rs for a complete example (requires parquet feature).
Component Discovery:
#[derive(Builder, facet::Facet)]#[facet(crate = facet)]pubstructComplexPipeline{analyzer:Predict<AnalyzeSignature>,// Additional Predict leaves are also optimizer-visiblesummarizer:Predict<SummarizeSignature>,// Non-predict fields are ignored by optimizersconfig:Config,}let visible = named_parameters_ref(&pipeline)?
.into_iter().map(|(path, _)| path).collect::<Vec<_>>();println!("optimizer-visible leaves: {:?}", visible);#[derive(Signature,Clone,Debug)]/// Analyze text for sentiment and key points.structAnalyze{#[input]text:String,#[output]sentiment:String,#[output]key_points:String,}#[derive(Signature,Clone,Debug)]/// Summarize the given key points.structSummarize{#[input]key_points:String,#[output]summary:String,}// Chain predictors with typed inputs/outputslet analyzer = Predict::<Analyze>::new();let summarizer = Predict::<Summarize>::new();let analysis = analyzer.call(AnalyzeInput{text: document.into()}).await?;let summary = summarizer.call(SummarizeInput{key_points: analysis.key_points.clone()}).await?;println!("Sentiment: {}", analysis.sentiment);println!("Summary: {}", summary.summary);Run the test suite:
# All tests
cargo test# Specific test
cargo test test_predictors
# With output
cargo test -- --nocapture
# Run examples
cargo run --example 01-simpleuse dspy_rs::ChainOfThought;// ChainOfThought wraps any signature, adding a `reasoning` fieldlet cot = ChainOfThought::<QA>::new();let result = cot.call(QAInput{question:"What is 2+2?".into(),}).await?;println!("Reasoning: {}", result.reasoning);println!("Answer: {}", result.answer);DSRs includes a tracing system that captures the dataflow through modules as a Directed Acyclic Graph (DAG). Wrap any execution in trace::trace() to capture the graph, then inspect its nodes and edges.
See examples/12-tracing.rs for a complete example.
| Feature | COPRO | MIPROv2 | GEPA |
|---|---|---|---|
| Approach | Iterative refinement | LLM-guided generation | Evolutionary search with textual feedback |
| Complexity | Simple | Advanced | Advanced |
| Best For | Quick optimization | Best results | Complex tasks with subtle failure modes |
| Training Data | Uses scores | Uses traces & descriptions | Uses rich textual feedback |
| Prompting Tips | No | Yes (15+ best practices) | No |
| Program Understanding | Basic | LLM-generated descriptions | LLM-judge feedback |
| Few-shot Examples | No | Yes (auto-selected) | No |
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)
When to use GEPA:
- Tasks where score alone doesn't explain what went wrong
- Need an LLM judge to provide actionable feedback
- Want Pareto-optimal exploration of the instruction space
We welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/krypticmouse/dsrs.git
cd dsrs
# Build the project
cargo build
# Run tests
cargo test# Run with examples
cargo run --example 01-simple
# Check formatting
cargo fmt -- --check
# Run clippy
cargo clippy -- -D warningsThis project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Inspired by the original DSPy framework
- Built with the amazing Rust ecosystem
- Special thanks to the DSPy community for the discussion and ideas
- MIPROv2 implementation
Star ⭐ this repo if you find it useful!