Quick Start Guide

Get up and running with DSPy.rb in minutes. This guide shows Ruby-idiomatic patterns for building AI applications.

Your First DSPy Program

Basic Prediction

require 'dspy'

# Define a signature for sentiment classification
class Classify < DSPy::Signature
  description "Classify sentiment of a given sentence."

  class Sentiment < T::Enum
    enums do
      Positive = new('positive')
      Negative = new('negative')
      Neutral = new('neutral')
    end
  end

  input do
    const :sentence, String
  end

  output do
    const :sentiment, Sentiment
    const :confidence, Float
  end
end

# Configure DSPy with your LLM
DSPy.configure do |c|
  c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
  # or use Google Gemini
  # c.lm = DSPy::LM.new('gemini/gemini-1.5-flash', api_key: ENV['GEMINI_API_KEY'])
  # or use Ollama for local models
  # c.lm = DSPy::LM.new('ollama/llama3.2')

  # Optional: Use BAML schema format for 80%+ token savings (new in v0.13.0)
  # c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'], schema_format: :baml)
end

# Create the predictor and run inference
classify = DSPy::Predict.new(Classify)
result = classify.call(sentence: "This book was super fun to read!")

puts result.sentiment    # => #<Sentiment::Positive>  
puts result.confidence   # => 0.85

Chain of Thought Reasoning

class AnswerPredictor < DSPy::Signature
  description "Provides a concise answer to the question"

  input do
    const :question, String
  end
  
  output do
    const :answer, String
  end
end

# Chain of thought automatically adds a 'reasoning' field to the output
qa_cot = DSPy::ChainOfThought.new(AnswerPredictor)
result = qa_cot.call(question: "Two dice are tossed. What is the probability that the sum equals two?")

puts result.reasoning  # => "There is only one way to get a sum of 2..."
puts result.answer     # => "1/36"

Multi-stage Pipelines

class Outline < DSPy::Signature
  description "Outline a thorough overview of a topic."

  input do
    const :topic, String
  end

  output do
    const :title, String
    const :sections, T::Array[String]
  end
end

class DraftSection < DSPy::Signature
  description "Draft a section of an article"

  input do
    const :topic, String
    const :title, String
    const :section, String
  end

  output do
    const :content, String
  end
end

class ArticleDrafter < DSPy::Module
  def initialize
    @build_outline = DSPy::ChainOfThought.new(Outline)
    @draft_section = DSPy::ChainOfThought.new(DraftSection)
  end

  def forward(topic:)
    outline = @build_outline.call(topic: topic)
    
    sections = outline.sections.map do |section|
      @draft_section.call(
        topic: topic,
        title: outline.title,
        section: section
      )
    end

    {
      title: outline.title,
      sections: sections.map(&:content)
    }
  end
end

# Use the pipeline
drafter = ArticleDrafter.new
article = drafter.forward(topic: "The impact of AI on software development")
puts article[:title]
puts article[:sections].first

Ruby-Idiomatic Examples

Working with Collections

DSPy.rb works naturally with Ruby’s Enumerable patterns:

# Process multiple items with Ruby's collection methods
class BatchProcessor < DSPy::Module
  def initialize
    @classifier = DSPy::Predict.new(Classify)
  end
  
  def process_batch(sentences)
    sentences.map { |sentence| @classifier.call(sentence: sentence) }
             .select { |result| result.confidence > 0.8 }
             .group_by(&:sentiment)
  end
end

# Usage
processor = BatchProcessor.new
results = processor.process_batch([
  "I love this product!",
  "This is terrible.",
  "It's okay, I guess."
])

# Access results by enum value
results[Classify::Sentiment::Positive]&.each { |r| puts r.sentence }

Block-Based Configuration

Configure DSPy components with Ruby blocks:

# Configure with blocks for cleaner syntax
DSPy.configure do |config|
  config.lm = DSPy::LM.new(
    'openai/gpt-4o-mini',
    api_key: ENV.fetch('OPENAI_API_KEY')
  )

  # Configure logging for observability
  config.logger = Dry.Logger(:dspy, formatter: :json) do |logger|
    logger.add_backend(stream: Rails.root.join('log', 'dspy.log'))
  end
end

Duck Typing with Tools

Create tools that follow Ruby’s duck typing principles with proper type signatures:

# Define a signature for the agent's task
class WeatherReport < DSPy::Signature
  description "Generate a weather report for a location"

  input do
    const :location, String
  end

  output do
    const :report, String
  end
end

# Any object that responds to #call can be a tool
# Best practice: Use Sorbet signatures for better LLM tool usage
class WeatherTool
  extend T::Sig

  sig { params(location: String).returns(T::Hash[Symbol, T.untyped]) }
  def call(location:)
    # In real app, this would call an API
    { temperature: 72, conditions: "sunny" }
  end
end

# Lambda tools for simple operations
calculator = ->(expression:) { eval(expression) }

# Use with ReAct agent
agent = DSPy::ReAct.new(
  WeatherReport,
  tools: {
    weather: WeatherTool.new,
    calculate: calculator
  }
)

Note: Sorbet type signatures help DSPy.rb generate accurate JSON schemas for the LLM, leading to more reliable tool usage and better handling of rich types.

Key Concepts

Signatures

Signatures define the interface for LLM operations:

class YourSignature < DSPy::Signature
  description "Clear description of what this does"
  
  input do
    const :input_field, String, description: "What this field represents"
  end
  
  output do
    const :output_field, String, description: "What the output should be"
  end
end

Predictors

Predictors execute signatures:

  • DSPy::Predict - Basic LLM completion
  • DSPy::ChainOfThought - Step-by-step reasoning
  • DSPy::ReAct - Tool-using agents
  • DSPy::CodeAct - Dynamic code execution agents

Modules

Modules compose multiple predictors into pipelines:

class YourModule < DSPy::Module
  def initialize
    @predictor1 = DSPy::Predict.new(Signature1)
    @predictor2 = DSPy::ChainOfThought.new(Signature2)
  end
  
  def forward(**inputs)
    result1 = @predictor1.call(**inputs)
    result2 = @predictor2.call(input: result1.output)
    
    { final_result: result2.output }
  end
end

Next Steps