Ruby's object-oriented design makes it exceptionally well-suited for building structured AI systems. In this installment of the Ruby for AI series, we will explore how to leverage classes, inheritance, modules, and mixins to create clean, reusable AI components.
Building Your First AI Class
Every Ruby object starts with a class. The initialize method acts as a constructor, and instance variables store object state.
class PromptTemplate
def initialize(template_string, variables = {})
@template_string = template_string
@variables = variables
@compiled = nil
end
def compile
@compiled = @template_string.gsub(/\{\{(\w+)\}\}/) do |match|
key = match[2..-3]
@variables[key] || match
end
end
def to_s
@compiled || compile
end
end
greeting = PromptTemplate.new("Hello, {{name}}! Your task is {{task}}.", {
"name" => "Ruby Developer",
"task" => "build AI agents"
})
puts greeting.compile
# => Hello, Ruby Developer! Your task is to build AI agents.
Controlling Access with attr_reader and attr_accessor
Ruby provides convenient macros for defining getters and setters. Use attr_reader for read-only attributes and attr_accessor when you need both read and write access.
class ChatMessage
attr_reader :role, :content, :timestamp
attr_accessor :metadata
def initialize(role:, content:, metadata: {})
@role = role
@content = content
@timestamp = Time.now
@metadata = metadata
end
def to_h
{ role: @role, content: @content, timestamp: @timestamp }
end
end
message = ChatMessage.new(role: "user", content: "Explain Ruby modules")
puts message.role # => user
message.metadata = { tokens: 15, model: "gpt-4" }
Inheritance for Specialized AI Components
Inheritance lets you create specialized versions of base classes. A ChatAgent hierarchy demonstrates this well.
class BaseAgent
attr_reader :name, :system_prompt
def initialize(name:, system_prompt: "You are a helpful assistant.")
@name = name
@system_prompt = system_prompt
@conversation_history = []
end
def add_message(role, content)
@conversation_history << ChatMessage.new(role: role, content: content)
end
def conversation_context
@conversation_history.map(&:to_h)
end
end
class CodeReviewAgent < BaseAgent
def initialize(name: "CodeReviewer")
super(
name: name,
system_prompt: "You are an expert code reviewer. Focus on security, performance, and readability."
)
@review_count = 0
end
def review_code(code_snippet)
@review_count += 1
add_message("user", "Please review this code:\n```
ruby\n#{code_snippet}\n
```")
# Simulate LLM call
"Review ##{@review_count}: Code analyzed for #{code_snippet.lines.count} lines."
end
end
reviewer = CodeReviewAgent.new
puts reviewer.review_code("def foo; bar; end")
Modules and Mixins: Shared Behavior
Modules in Ruby serve two purposes: namespaces and mixins. For AI development, mixins let you share capabilities across unrelated classes.
module TokenCountable
def estimate_tokens(text)
# Rough approximation: 1 token ~= 4 characters
(text.length / 4.0).ceil
end
def token_budget_remaining(budget)
budget - estimate_tokens(to_s)
end
end
module RateLimitable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def rate_limit(calls:, per: 60)
@max_calls = calls
@window_seconds = per
@call_timestamps = []
end
def can_call?
now = Time.now
@call_timestamps.reject! { |t| now - t > @window_seconds }
@call_timestamps.length < @max_calls
end
end
def record_call
self.class.instance_variable_get(:@call_timestamps) << Time.now
end
end
Include vs Extend: Choosing Your Approach
Understanding the distinction between include and extend is crucial for proper module design.
class LLMClient
include TokenCountable # Instance methods
extend RateLimitable # Class methods
rate_limit calls: 100, per: 60
attr_reader :last_prompt
def initialize(api_key)
@api_key = api_key
@last_prompt = ""
end
def complete(prompt)
raise "Rate limit exceeded" unless self.class.can_call?
@last_prompt = prompt
record_call
puts "Tokens estimated: #{estimate_tokens(prompt)}"
"Generated response for: #{prompt[0..50]}..."
end
def to_s
@last_prompt
end
end
client = LLMClient.new("sk-...")
puts client.complete("Write a Ruby method to parse JSON")
puts "Remaining budget: #{client.token_budget_remaining(4000)} tokens"
A Complete AI Agent Example
Here is a practical implementation combining all concepts: a configurable agent system using modules for capabilities and inheritance for specialization.
module ToolUsing
def register_tool(name, &block)
@tools ||= {}
@tools[name] = block
end
def execute_tool(name, *args)
@tools&.[](name)&.call(*args) || "Tool #{name} not found"
end
def available_tools
@tools&.keys || []
end
end
class ReActAgent < BaseAgent
include ToolUsing
attr_reader :thoughts
def initialize(name:, system_prompt: nil)
super(
name: name,
system_prompt: system_prompt || "You are a ReAct agent. Think step by step."
)
@thoughts = []
setup_default_tools
end
def think(observation)
@thoughts << observation
"Thought #{@thoughts.length}: #{observation}"
end
def act
"Acting based on #{@thoughts.length} thoughts..."
end
private
def setup_default_tools
register_tool(:search) { |query| "Searching for: #{query}" }
register_tool(:calculate) { |expr| eval(expr).to_s rescue "Error" }
end
end
agent = ReActAgent.new(name: "ResearchAssistant")
agent.think("I need to find Ruby documentation")
puts agent.execute_tool(:search, "Ruby modules")
puts agent.available_tools.inspect
puts agent.act
Conclusion
Object-oriented Ruby provides powerful abstractions for AI development. Classes encapsulate state and behavior, inheritance enables specialization, and modules offer flexible code sharing through mixins. These patterns form the foundation for larger systems explored later in this Ruby for AI series, including LLM client libraries and multi-agent orchestration frameworks.
Top comments (0)