Ruby and the Art of Talking to Machines

Illustration of a person programming in Ruby

There's a quiet revolution happening in the Ruby community. While the headlines focus on Python's dominance in AI and the JavaScript ecosystem's relentless expansion, Ruby developers have been doing something rather elegant: making large language models feel like a natural extension of the language itself.

The State of LLM Access Across Languages

If you've worked with LLMs in Python, you've written something that looks like this:

Python
import openai

client = openai.OpenAI(api_key="sk-...")

response = client.chat.completions.create(
    model="gpt-4",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain quantum computing."}
    ],
    temperature=0.7,
    max_tokens=1024
)

print(response.choices[0].message.content)

It works. It's functional. But look at it, nested dictionaries, explicit role strings, method chains that read like legal documents. There's nothing wrong with it, but there's nothing that delights, either.

Now consider the equivalent in TypeScript:

TypeScript
import OpenAI from 'openai';

const client = new OpenAI({ apiKey: 'sk-...' });

const response = await client.chat.completions.create({
  model: 'gpt-4',
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'Explain quantum computing.' }
  ],
  temperature: 0.7,
  max_tokens: 1024
});

console.log(response.choices[0].message.content);

Similar structure, similar verbosity. The pattern is always the same: create a client, build a messages array with explicit role strings, call a method, dig into the response object. It's fine. Everything is fine. But it's not joyful.

The Ruby way

Now, Look at Ruby

Here's the same task in Ruby. Notice not just what it does, but how it feels:

Ruby
require "openai"

client = OpenAI::Client.new(access_token: "sk-...")

response = client.chat(
  parameters: {
    model: "gpt-4",
    messages: [
      { role: "system", content: "You are a helpful assistant." },
      { role: "user", content: "Explain quantum computing." }
    ],
    temperature: 0.7,
    max_tokens: 1024
  }
)

puts response.dig("choices", 0, "message", "content")

Already, something feels different. The require "openai" is honest and unadorned. The method is called chat, not chat.completions.create, a three-word method chain that reads like a shipping manifest. Ruby favours directness.

But this is just the official SDK. The real magic happens when the community starts wrapping these APIs in Ruby-idiomatic ways.

The Gem Ecosystem: Where Elegance Compounds

Ruby's gem ecosystem has produced several LLM libraries that feel genuinely different from what you'd find in other languages. Take the ruby-openai gem, or community abstractions that wrap multiple providers behind a single, beautifully simple interface.

Ruby, Multi-provider abstraction
require "ai_client"

# Switch providers by changing one symbol
ai = AIClient.new(provider: :openai, model: "gpt-4")
# ai = AIClient.new(provider: :anthropic, model: "claude-sonnet-4-20250514")
# ai = AIClient.new(provider: :google, model: "gemini-2.0-flash")

# Stream a response with a block, because Ruby loves blocks
ai.chat("Write a haiku about recursion") do |chunk|
  print chunk
end

# Or get the full response as a clean object
response = ai.ask(
  system: "You are a poet.",
  prompt: "Write a sonnet about Ruby programming.",
  temperature: 0.8
)

puts response.content    # The text
puts response.tokens     # Token usage
puts response.model      # Which model answered

Notice the block syntax. This is quintessential Ruby, passing a block to a method for streaming responses feels as natural as each or map. The language was practically designed for this pattern, even though Matz couldn't have known streaming AI responses would be a thing thirty years later.

Why Ruby Suits LLM Work

There are several reasons Ruby feels particularly well-suited to LLM interactions, and they all stem from the same philosophy: developer happiness.

Blocks and Procs

Streaming tokens from an LLM maps perfectly onto Ruby's block syntax. Every streaming implementation in Ruby reads like pseudocode. Python uses generators and yield; JavaScript uses async iterators. Ruby just uses do |token| ... end.

Open Classes and Monkey Patching

Ruby's open classes mean you can extend LLM response objects with convenience methods without touching the original gem. Add response.to_markdown or response.extract_json in three lines. No subclassing, no wrappers, no ceremony.

Digging into Hashes

LLM APIs return deeply nested JSON. Ruby's dig method lets you traverse these structures with grace: response.dig("choices", 0, "message", "content"). It's not perfect, but it's far more pleasant than chaining bracket accessors in Python.

DSL-Friendly Design

Ruby excels at building domain-specific languages. LLM interaction is ripe for DSLs, and the community has delivered. Libraries that let you define prompt templates, chain conversations, and manage context with syntax that reads like English.

A Conversation Chain, the Ruby Way

Here's a more complex example, a multi-turn conversation with context management, prompt templates, and structured output. In Python, this would be a class with decorators and type hints. In Ruby, it's almost poetry:

Ruby, Conversation with context
class Conversation
  include AI::Helpers

  def initialize(system_prompt:, model: "gpt-4")
    @messages = [{ role: "system", content: system_prompt }]
    @model = model
  end

  def ask(question)
    @messages << { role: "user", content: question }

    response = AI.chat(
      model: @model,
      messages: @messages,
      temperature: 0.7
    )

    answer = response.content
    @messages << { role: "assistant", content: answer }

    answer
  end

  def summarize
    ask("Summarise our conversation so far in three bullet points.")
  end

  def to_s
    @messages.reject { |m| m[:role] == "system" }
             .map { |m| "#{m[:role].capitalize}: #{m[:content]}" }
             .join("\n\n")
  end
end

# Usage, reads almost like English
bot = Conversation.new(
  system_prompt: "You are a helpful Ruby programming tutor.",
  model: "gpt-4"
)

puts bot.ask("What are mixins in Ruby?")
puts bot.ask("How do they differ from inheritance?")
puts bot.summarise

There's no ceremony here. No abstract base classes, no dependency injection, no decorator patterns. Just a Ruby class that does what it says. The include AI::Helpers line pulls in convenience methods. The ask method is self-documenting. And the to_s method lets you puts the entire conversation because of course it does, this is Ruby.

There's a gem for that

The Honest Comparison

I should be fair. Python's advantage in the AI/ML space is real and well-earned. The ecosystem is deeper: PyTorch, Hugging Face, LangChain, these are mature, battle-tested libraries with enormous communities. If you're training models, fine-tuning, or building production ML pipelines, Python is the pragmatic choice.

But for the increasingly common task of integrating LLMs into applications, building chatbots, content generators, code assistants, and intelligent workflows, Ruby offers something Python often doesn't: code that you actually want to read.

And that matters more than people think. Software is read far more often than it is written. A codebase that brings joy to read is a codebase that gets maintained, extended, and loved. Ruby has always understood this.

"Ruby doesn't just let you talk to machines. It makes the conversation feel like a dialogue between friends rather than a series of formal requests."

What's Available Now

If you're curious about exploring LLM work in Ruby, the ecosystem is more capable than you might expect:

ruby-openai

The official OpenAI Ruby SDK. Clean, well-documented, and supports streaming, embeddings, fine-tuning, and the full API surface.

anthropic SDK

Claude access from Ruby. Same Ruby-idiomatic design patterns, full streaming support.

langchainrb

Ruby's answer to LangChain. Agent frameworks, chain-of-thought prompting, tool use, and memory, all wrapped in beautiful Ruby syntax.

ruby_llm

A unified interface across OpenAI, Anthropic, and Google. Provider-agnostic with a focus on simplicity. Change one symbol, change your provider.

The Bigger Picture

Ruby's role in AI isn't about displacing Python. It's about proving that developer experience and cutting-edge technology aren't mutually exclusive. For decades, Ruby has been the language that makes you feel good about writing code. Now it's doing the same for the AI integration layer.

And there's something rather lovely about that. In an industry obsessed with raw performance benchmarks and framework du jour, Ruby quietly demonstrates that how code feels matters, not just how it runs. That elegance is a feature, not a luxury. And that Matz's original vision of a language that optimises for human happiness turns out to be remarkably well-suited for an era where humans and machines are learning to collaborate.

โ† AI Augmentation All Articles