Predictors
Predictors are the execution engines that take your signatures and generate structured results using language models. DSPy.rb provides three predictor types for different use cases.
DSPy::Predict
The foundational predictor that executes signatures directly with the language model.
Basic Usage
class ClassifyText < DSPy::Signature
description "Classify text sentiment and extract key topics"
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 :topics, T::Array[String]
const :confidence, Float
end
end
# Create and use the predictor
classifier = DSPy::Predict.new(ClassifyText)
result = classifier.call(text: "I absolutely love the new features in this app!")
puts result.sentiment # => #<Sentiment::Positive>
puts result.topics # => ["app", "features"]
puts result.confidence # => 0.92
Basic Configuration
# Basic usage - uses global language model
predictor = DSPy::Predict.new(ClassifyText)
# Basic usage - uses global language model
predictor = DSPy::Predict.new(ClassifyText)
DSPy::ChainOfThought
Adds step-by-step reasoning to improve accuracy on complex tasks. The model first generates reasoning, then produces the final answer.
When to Use ChainOfThought
- Complex analysis requiring multiple steps
- Mathematical or logical reasoning
- Tasks where showing work improves accuracy
- When you need explainable AI decisions
Basic Usage
class SolveMathProblem < DSPy::Signature
description "Solve mathematical word problems step by step"
input do
const :problem, String
end
output do
const :answer, String
const :solution_steps, T::Array[String]
end
end
solver = DSPy::ChainOfThought.new(SolveMathProblem)
result = solver.call(problem: "Sarah has 15 apples. She gives 7 to her friend and buys 12 more. How many apples does she have now?")
puts result.reasoning # => "Let me work through this step by step:\n1. Sarah starts with 15 apples\n2. She gives away 7 apples: 15 - 7 = 8 apples\n3. She buys 12 more: 8 + 12 = 20 apples\nTherefore, Sarah has 20 apples."
puts result.answer # => "20 apples"
puts result.solution_steps # => ["Start: 15 apples", "Give away 7: 15-7=8", "Buy 12 more: 8+12=20"]
Working with Reasoning
class ComplexAnalysis < DSPy::Signature
description "Perform business analysis with reasoning"
input do
const :scenario, String
const :constraints, T::Array[String]
end
output do
const :recommendation, String
const :risks, T::Array[String]
end
end
analyzer = DSPy::ChainOfThought.new(ComplexAnalysis)
result = analyzer.call(
scenario: "Launching a new product in a competitive market",
constraints: ["Limited budget", "6-month timeline"]
)
# ChainOfThought automatically adds reasoning field
puts result.reasoning
# => "Let me analyze this step by step:
# 1. Market Analysis: [analysis]
# 2. Strategy Development: [approach]
# ..."
puts result.recommendation
# => "Launch with a focused MVP approach..."
DSPy::ReAct
Combines reasoning with action - enables agents that use tools and make decisions based on external information.
Tool Definition
class WeatherTool < DSPy::Tools::Base
extend T::Sig
tool_name "weather"
tool_description "Get weather information"
sig { params(location: String).returns(String) }
def call(location:)
# Simulate weather API call
{
location: location,
temperature: rand(60..85),
condition: ['sunny', 'cloudy', 'rainy'].sample
}.to_json
end
end
class SearchTool < DSPy::Tools::Base
extend T::Sig
tool_name "search"
tool_description "Search the web"
sig { params(query: String).returns(String) }
def call(query:)
# Simulate web search
[
{ name: "Result 1", snippet: "Information about #{query}" },
{ name: "Result 2", snippet: "More details on #{query}" }
].to_json
end
end
ReAct Agent Usage
class TravelAssistant < DSPy::Signature
description "Help users plan travel"
input do
const :destination, String
const :interests, T::Array[String]
end
output do
const :recommendations, String
end
end
agent = DSPy::ReAct.new(
TravelAssistant,
tools: [WeatherTool.new, SearchTool.new],
max_iterations: 5
)
result = agent.call(
destination: "Tokyo, Japan",
interests: ["food", "temples"]
)
# The agent will:
# 1. Think: "I need to check the weather for Tokyo"
# 2. Act: weather({"location": "Tokyo, Japan"})
# 3. Think: "Now I should search for food and temple recommendations"
# 4. Act: search({"query": "best food Tokyo"})
# 5. Think: "Based on research, I can make recommendations"
# 6. Provide final response
puts result.recommendations
# => "Visit Senso-ji Temple early morning. Try ramen at local shops in Shibuya..."
# Access the reasoning history
puts result.history
# => Array of reasoning steps, actions, and observations
puts result.iterations # => 3
puts result.tools_used # => ["weather", "search"]
Custom Tool Integration
class DatabaseTool < DSPy::Tools::Base
extend T::Sig
tool_name "database"
tool_description "Query user database"
sig { params(connection: T.untyped).void }
def initialize(connection)
super()
@db = connection
end
sig { params(query: String).returns(String) }
def call(query:)
# Simple database query
result = @db.execute(query)
result.to_json
end
end
class CustomerService < DSPy::Signature
description "Provide customer service"
input do
const :customer_query, String
end
output do
const :response, String
end
end
service_agent = DSPy::ReAct.new(
CustomerService,
tools: [DatabaseTool.new(database_connection)],
max_iterations: 3
)
DSPy::CodeAct
CodeAct enables agents to write and execute Ruby code dynamically to solve complex tasks. Unlike ReAct which uses predefined tools, CodeAct generates executable Ruby code that can perform calculations, data manipulation, and complex operations.
When to Use CodeAct
- Mathematical computations and data analysis
- Complex data transformations
- Dynamic algorithm implementation
- Tasks requiring control flow and variable storage
- Scenarios where predefined tools are insufficient
Basic Usage
class DataAnalysis < DSPy::Signature
description "Analyze data using Ruby code"
input do
const :task, String
end
output do
const :solution, String
end
end
analyzer = DSPy::CodeAct.new(DataAnalysis, max_iterations: 5)
result = analyzer.call(task: "Calculate the average of numbers 1 through 100")
puts result.solution
# => "50.5"
# Access the execution history
puts result.history
# => Array showing step-by-step code execution
puts result.iterations # => 2
puts result.execution_context # => Variables and results from execution
Code Execution Flow
class MathSolver < DSPy::Signature
description "Solve mathematical problems with Ruby code"
input do
const :problem, String
end
output do
const :answer, String
end
end
solver = DSPy::CodeAct.new(MathSolver, max_iterations: 10)
result = solver.call(
problem: "Find the sum of all prime numbers less than 100"
)
# The agent will:
# 1. Think: "I need to find prime numbers less than 100"
# 2. Code: def is_prime?(n); ...; end
# 3. Execute: Method defined successfully
# 4. Think: "Now I'll find all primes and sum them"
# 5. Code: primes = (2...100).select { |n| is_prime?(n) }; primes.sum
# 6. Execute: 1060
# 7. Finish: Return the result
puts result.answer # => "1060"
puts result.iterations # => 3
# Examine the code execution history
result.history.each_with_index do |entry, i|
puts "Step #{entry[:step]}:"
puts " Thought: #{entry[:thought]}"
puts " Code: #{entry[:ruby_code]}"
puts " Result: #{entry[:execution_result]}"
puts " Error: #{entry[:error_message]}" if entry[:error_message]
end
Error Handling and Recovery
class RobustCalculator < DSPy::Signature
description "Perform calculations with error recovery"
input do
const :expression, String
end
output do
const :result, String
end
end
calculator = DSPy::CodeAct.new(RobustCalculator, max_iterations: 5)
result = calculator.call(expression: "Calculate factorial of 10")
# If the agent writes incorrect code, it can:
# 1. Detect the error from execution feedback
# 2. Analyze the error message
# 3. Write corrected code
# 4. Successfully complete the task
puts result.result # => "3628800"
Complex Data Processing
class DataProcessor < DSPy::Signature
description "Process and analyze data structures"
input do
const :data_task, String
end
output do
const :processed_result, String
end
end
processor = DSPy::CodeAct.new(DataProcessor, max_iterations: 8)
result = processor.call(
data_task: "Create a hash mapping letters to their ASCII values for 'HELLO'"
)
# The agent can:
# - Create complex data structures
# - Iterate through collections
# - Apply transformations
# - Format output appropriately
puts result.processed_result
# => "{'H' => 72, 'E' => 69, 'L' => 76, 'L' => 76, 'O' => 79}"
Predictor Comparison
Performance Characteristics
Predictor | Speed | Use Case | Token Usage |
---|---|---|---|
Predict | Fastest | Simple classification, extraction | Low |
ChainOfThought | Moderate | Complex reasoning, analysis | Medium-High |
ReAct | Slower | Multi-step tasks, tool usage | High |
CodeAct | Slowest | Dynamic programming, calculations | Very High |
Choosing the Right Predictor
# Simple, fast tasks
quick_classifier = DSPy::Predict.new(SimpleClassification)
# Complex reasoning needed
analyst = DSPy::ChainOfThought.new(ComplexAnalysis)
# Multi-step tasks with external data
agent = DSPy::ReAct.new(AgentTask, tools: [tool1, tool2])
# Dynamic programming and calculations
programmer = DSPy::CodeAct.new(ProgrammingTask, max_iterations: 10)
Error Handling
Basic Error Handling
class RobustPredictor
def initialize(signature)
@primary = DSPy::ChainOfThought.new(signature)
@fallback = DSPy::Predict.new(signature)
end
def call(input)
@primary.call(input)
rescue StandardError => e
puts "Primary predictor failed: #{e.message}"
@fallback.call(input)
end
end
Input Validation
class ValidatedPredictor
def initialize(signature)
@predictor = DSPy::Predict.new(signature)
@signature = signature
end
def call(input)
# Validate input structure
@signature.input_struct_class.new(**input)
# Call predictor
@predictor.call(input)
rescue ArgumentError => e
raise DSPy::PredictionInvalidError.new({ input: e.message })
end
end
Prompt Optimization
Working with Examples
# Create predictor with examples
classifier = DSPy::Predict.new(SentimentAnalysis)
# Add few-shot examples
examples = [
DSPy::FewShotExample.new(
input: { text: "I love this product!" },
output: { sentiment: "positive", confidence: 0.9 }
),
DSPy::FewShotExample.new(
input: { text: "This is terrible." },
output: { sentiment: "negative", confidence: 0.8 }
)
]
optimized_classifier = classifier.with_examples(examples)
Custom Instructions
# Modify instruction
predictor = DSPy::Predict.new(TextClassifier)
optimized = predictor.with_instruction(
"You are an expert classifier. Be precise and confident."
)
result = optimized.call(text: "Sample text")
Testing Predictors
Unit Tests
RSpec.describe DSPy::Predict do
let(:signature) { SimpleClassification }
let(:predictor) { described_class.new(signature) }
describe "#call" do
it "returns structured results" do
result = predictor.call(text: "Sample text")
expect(result).to respond_to(:classification)
expect(result).to respond_to(:confidence)
end
it "validates input structure" do
expect {
predictor.call(invalid_field: "value")
}.to raise_error(DSPy::PredictionInvalidError)
end
end
end
Testing ChainOfThought
RSpec.describe DSPy::ChainOfThought do
let(:predictor) { described_class.new(ComplexAnalysis) }
it "includes reasoning in output" do
result = predictor.call(
scenario: "Market expansion",
constraints: ["Limited budget"]
)
expect(result).to respond_to(:reasoning)
expect(result.reasoning).to be_a(String)
expect(result.reasoning).not_to be_empty
end
end
Best Practices
1. Choose the Right Predictor
# Simple extraction → Predict
email_extractor = DSPy::Predict.new(ExtractEmails)
# Complex analysis → ChainOfThought
business_analyzer = DSPy::ChainOfThought.new(BusinessAnalysis)
# Multi-step with tools → ReAct
research_agent = DSPy::ReAct.new(ResearchTask, tools: [SearchTool.new])
2. Handle Errors Gracefully
class ProductionPredictor
def call(input)
@predictor.call(input)
rescue DSPy::PredictionInvalidError => e
handle_validation_error(e)
rescue StandardError => e
handle_unexpected_error(e)
end
end
3. Use Built-in Instrumentation
# Instrumentation is automatic - check DSPy.config.instrumentation
# Events are emitted for:
# - dspy.predict
# - dspy.chain_of_thought
# - dspy.react
# - dspy.codeact (includes code_execution events)
# Enable logging to see events
DSPy.configure do |config|
config.instrumentation.enabled = true
config.instrumentation.subscribers = ['logger']
end
Predictors provide the core execution capabilities for DSPy applications with built-in instrumentation and type safety.