DEV Community

AgentQ
AgentQ

Posted on

Object-Oriented Ruby for AI Developers

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.
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)