Core Concepts
DSPy.rb is built around composable, type-safe modules that make LLM programming predictable and reliable. This guide covers the fundamental concepts you need to understand to build effective DSPy applications.
Signatures: Defining LLM Interfaces
Signatures are the foundation of DSPy. They define typed interfaces for LLM operations, specifying inputs, outputs, and behavior descriptions.
class ClassifyText < DSPy::Signature
description "Classify the sentiment and topic of the given text."
class Sentiment < T::Enum
enums do
Positive = new('positive')
Negative = new('negative')
Neutral = new('neutral')
end
end
input do
const :text, String
end
output do
const :sentiment, Sentiment
const :topic, String
const :confidence, Float
end
end
Key Features
- Type Safety: Signatures use Sorbet types to ensure runtime type checking
- Structured Outputs: Define complex output schemas with enums, objects, and arrays
- Self-Documenting: Descriptions provide context to the LLM about the task
- Composable: Signatures can be reused across different predictors and modules
Predictors: Basic LLM Operations
Predictors are the basic building blocks that execute signatures using language models.
DSPy::Predict
The simplest predictor that takes a signature and returns structured results:
classifier = DSPy::Predict.new(ClassifyText)
result = classifier.call(text: "I love this new restaurant!")
puts result.sentiment # => #<Sentiment::Positive>
puts result.topic # => "restaurant"
puts result.confidence # => 0.92
DSPy::ChainOfThought
Adds step-by-step reasoning to improve accuracy on complex tasks:
class SolveMath < DSPy::Signature
description "Solve the given math problem step by step."
input { const :problem, String }
output { const :solution, String }
end
solver = DSPy::ChainOfThought.new(SolveMath)
result = solver.call(problem: "If John has 15 apples and gives away 7, then buys 12 more, how many does he have?")
puts result.reasoning # => "First, John starts with 15 apples..."
puts result.solution # => "20 apples"
DSPy::ReAct
Combines reasoning with action - perfect for agents that need to use tools:
class Calculator < DSPy::Tools::Base
extend T::Sig
tool_name "calculator"
tool_description "Perform mathematical calculations"
sig { params(operation: String, a: Float, b: Float).returns(String) }
def call(operation:, a:, b:)
case operation
when "add"
(a + b).to_s
when "multiply"
(a * b).to_s
else
"Unknown operation"
end
end
end
class MathAgent < DSPy::Signature
description "Solve math problems using available tools."
input { const :problem, String }
output { const :answer, String }
end
agent = DSPy::ReAct.new(MathAgent, tools: [Calculator.new])
result = agent.call(problem: "What is (15 + 7) * 3?")
# The agent will reason about the problem and use tools
puts result.answer # => "66"
Modules: Composing Complex Workflows
Modules let you compose multiple predictors into sophisticated pipelines.
class DocumentProcessor < DSPy::Module
def initialize
@classifier = DSPy::Predict.new(ClassifyText)
@summarizer = DSPy::Predict.new(SummarizeText)
end
def call(document)
classification = @classifier.call(text: document)
summary = @summarizer.call(text: document)
{
classification: classification,
summary: summary,
processed_at: Time.current
}
end
end
Examples: Type-Safe Training Data
Examples provide type-safe training data for optimization:
examples = [
DSPy::Example.new(
signature_class: ClassifyText,
input: { text: "This movie was amazing!" },
expected: { sentiment: ClassifyText::Sentiment::Positive, confidence: 0.9 }
),
DSPy::Example.new(
signature_class: ClassifyText,
input: { text: "Terrible service, would not recommend." },
expected: { sentiment: ClassifyText::Sentiment::Negative, confidence: 0.95 }
)
]
# Use examples for evaluation
evaluator = DSPy::Evaluate.new(metric: :exact_match)
results = evaluator.evaluate(examples: examples) do |example|
classifier.call(example.input_values)
end
Configuration: Setting Up Your Environment
Configure DSPy with your preferred language model:
DSPy.configure do |config|
config.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
# Optional: Configure instrumentation
config.instrumentation.enabled = true
config.instrumentation.subscribers = [:logger, :otel]
end
Language Model Options
DSPy.rb supports multiple LLM providers:
# OpenAI
config.lm = DSPy::LM.new('openai/gpt-4o', api_key: ENV['OPENAI_API_KEY'])
# Anthropic
config.lm = DSPy::LM.new('anthropic/claude-3-sonnet', api_key: ENV['ANTHROPIC_API_KEY'])
# Per-module language model configuration
predictor = DSPy::Predict.new(ClassifyText)
predictor.configure do |config|
config.lm = DSPy::LM.new('openai/gpt-4o') # Override global LM for this instance
end
Error Handling and Validation
DSPy provides comprehensive error handling:
begin
result = classifier.call(text: "Sample text")
rescue DSPy::PredictionInvalidError => e
puts "Validation failed: #{e.message}"
puts "Errors: #{e.errors}"
rescue StandardError => e
puts "Unexpected error: #{e.message}"
end
Best Practices
1. Start Simple
Begin with basic Predict modules and add complexity gradually:
# Start with this
basic_classifier = DSPy::Predict.new(ClassifyText)
# Add reasoning when needed
reasoning_classifier = DSPy::ChainOfThought.new(ClassifyText)
# Add tools for complex tasks
agent_classifier = DSPy::ReAct.new(ComplexClassification, tools: [WebSearch.new])
2. Use Clear Descriptions
Your signature descriptions directly impact LLM performance:
# Good: Clear and specific
description "Classify the sentiment of customer feedback as positive, negative, or neutral. Consider context and nuance."
# Bad: Vague
description "Classify text"
3. Leverage Type Safety
Use Sorbet types to catch errors early:
class Priority < T::Enum
enums do
Low = new('low')
Medium = new('medium')
High = new('high')
Critical = new('critical')
end
end
output do
const :priority, Priority # Type-safe enum
const :confidence, Float # Runtime validation
const :metadata, T::Hash[String, String] # Structured data
end
4. Compose Thoughtfully
Break complex workflows into reusable components:
class EmailProcessor < DSPy::Module
def initialize
@classifier = DSPy::Predict.new(ClassifyEmail)
@responder = DSPy::ChainOfThought.new(DraftResponse)
@validator = DSPy::Predict.new(ValidateResponse)
end
def call(email)
classification = @classifier.call(email: email)
return early_response(classification) if simple_case?(classification)
draft = @responder.call(email: email, context: classification)
validation = @validator.call(response: draft.response)
finalize_response(draft, validation)
end
end
Next Steps
- Signatures & Types - Deep dive into signature design
- Predictors - Master the different predictor types
- Modules & Pipelines - Build complex workflows
- Examples & Validation - Create robust training data
Common Patterns
Multi-Step Processing
class DocumentAnalyzer < DSPy::Module
def call(document)
summary = @summarizer.call(text: document)
topics = @topic_extractor.call(text: summary.summary)
sentiment = @sentiment_analyzer.call(text: document)
combine_results(summary, topics, sentiment)
end
end
Conditional Routing
class SmartClassifier < DSPy::Module
def call(text)
complexity = @complexity_analyzer.call(text: text)
if complexity.level == 'high'
@reasoning_classifier.call(text: text)
else
@simple_classifier.call(text: text)
end
end
end
Error Recovery
class RobustProcessor < DSPy::Module
def call(input)
@primary_processor.call(input)
rescue StandardError
@fallback_processor.call(input)
end
end
Understanding these core concepts will enable you to build reliable LLM applications with DSPy.rb. The type system keeps you safe, the modular design keeps your code clean, and the optimization tools help you achieve production-ready performance.