
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,ChatAdapter,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?,ChatAdapter,);// 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)]pubstructCustomModule{predictor:Predict<TranslationSignature>,}implModuleforCustomModule{asyncfnforward(&self,inputs:Example) -> Result<Prediction>{// Your custom logic hereself.predictor.forward(inputs).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?;implEvaluatorforMyModule{asyncfnmetric(&self,example:&Example,prediction:&Prediction) -> f32{// Define your custom metric logiclet expected = example.get("answer",None);let predicted = prediction.get("answer",None);// Example: Exact match metricif expected.to_lowercase() == predicted.to_lowercase(){1.0}else{0.0}}}// Evaluate your modulelet test_examples = load_test_data();let module = MyModule::new();// Automatically runs predictions and computes average metriclet score = module.evaluate(test_examples).await;println!("Average score: {}", score);DSRs provides two powerful optimizers:
COPRO (Collaborative Prompt Optimization)
#[derive(Optimizable)]pubstructMyModule{#[parameter]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();// Compile optimizes the module in-placeletmut module = MyModule::new();
optimizer.compile(&mut module, train_examples).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).await?;See examples/08-optimize-mipro.rs for a complete example (requires parquet feature).
Component Freezing:
// The Optimizable derive macro automatically implements the trait and marks Module Optimizable#[derive(Builder,Optimizable)]pubstructComplexPipeline{#[parameter]// Mark optimizable componentsanalyzer:Predict<AnalyzeSignature>,// Non-parameter fields won't be optimizedsummarizer:Predict<SummarizeSignature>,// Non-parameter fields won't be optimizedconfig:Config,}use dsrs::prelude::*;#[Signature]structAnalyzeSignature{#[input]pubtext:String,#[output]pubsentiment:String,#[output]pubkey_points:String,}#[Signature]structSummarizeSignature{#[input]pubkey_points:String,#[output]pubsummary:String,}#[derive(Builder)]pubstructAnalysisPipeline{analyzer:Predict,summarizer:Predict,}implModuleforAnalysisPipeline{asyncfnforward(&self,inputs:Example) -> Result<Prediction>{// Step 1: Analyze the textlet analysis = self.analyzer.forward(inputs).await?;// Step 2: Summarize key pointslet summary_input = example!{"key_points":"input" => analysis.get("key_points",None),};let summary = self.summarizer.forward(summary_input).await?;// Combine resultsOk(prediction!{"sentiment" => analysis.get("sentiment",None),"key_points" => analysis.get("key_points",None),"summary" => summary.get("summary",None),})}}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-simple#[Signature(cot)]// Enable CoT with attributestructComplexReasoningSignature{#[input(desc="Question")pubproblem:String,#[output]pubsolution:String,}The tracing system allows you to capture the dataflow through modules and build a Directed Acyclic Graph (DAG) representation of the execution flow.
The tracing system consists of:
- Graph: A DAG structure representing nodes (modules/predictors) and edges (data dependencies)
- Trace Context: Captures execution traces and builds the DAG using
tokio::task_local - Executor: Executes captured graphs with new inputs
Use trace::trace() to wrap your execution and capture the DAG:
use dspy_rs::{trace, example,Predict,Signature};#[Signature]structQASignature{#[input]pubquestion:String,#[output]pubanswer:String,}let predictor = Predict::new(QASignature::new());let example = example!{"question":"input" => "Hello",};// Trace the executionlet(result, graph) = trace::trace(|| async{
predictor.forward(example).await}).await;// Inspect the graphprintln!("Graph Nodes: {}", graph.nodes.len());for node in&graph.nodes{println!("Node {}: Type={:?}, Inputs={:?}", node.id, node.node_type, node.inputs);}// Execute the graph with new inputlet executor = trace::Executor::new(graph);let new_input = example!{"question":"input" => "What is the capital of France?",};let predictions = executor.execute(new_input).await?;When building pipelines, use get_tracked() to preserve data lineage:
let prediction = predictor.forward(inputs).await?;let answer = prediction.get_tracked("answer");// Preserves source node info// The example! macro automatically detects tracked values and records Map nodeslet next_input = example!{"answer":"input" => answer.clone(),};Node: Represents a single execution step:
id: Unique identifiernode_type: Type of node (Root,Predict,Map,Operator)inputs: IDs of parent nodesoutput: Output Predictioninput_data: Input Example (for root nodes)
Graph: Contains all nodes and provides execution capabilities:
nodes: Vector of all nodesExecutor: Can execute the graph with new inputs
The graph is fully modifiable - you can:
- Split nodes (add intermediate steps)
- Remove nodes
- Fuse nodes (combine operations)
- Insert nodes between existing ones
- Modify node configurations (signatures, instructions)
// Example: Modify a node's signatureifletSome(node) = graph.nodes.get_mut(1){ifletNodeType::Predict{ signature, .. } = &mut node.node_type{// Modify signature instruction, demos, etc.}}See examples/12-tracing.rs for a complete example demonstrating:
- Tracing module execution
- Inspecting the DAG
- Executing graphs with new inputs
- Modifying graph structure
| 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)
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!