Constructing AI Applications in Ruby

-

Table of Contents

Introduction

It’s infrequently that you simply hear the Ruby language mentioned when discussing AI.

Python, after all, is the king on this world, and for good reason. The community has coalesced across the language. Most model training is completed in PyTorch or TensorFlow as of late. Scikit-learn and Keras are also extremely popular. RAG frameworks reminiscent of LangChain and LlamaIndex cater primarily to Python.

Nevertheless, on the subject of constructing web applications with AI integration, I consider Ruby is the higher language.

Because the co-founder of an agency dedicated to constructing MVPs with generative AI integration, I steadily hear potential clients complaining about two things:

  • Applications take too long to construct
  • Developers are quoting insane prices to construct custom web apps

These complaints have a typical source: complexity. Modern web apps have so much more complexity in them than in the nice ol’ days. But why is that this? Are the advantages brought by complexity price the associated fee?

I believed spas were alleged to be relaxing?

One big piece of the puzzle is the recent rise of single-page applications (SPAs). The preferred stack used today in constructing modern SPAs is MERN (MongoDB, Express.js, React.js, Node.js). The stack is popular for a number of reasons:

  • It’s a JavaScript-only stack, across each front-end and back-end. Having to only code in just one language is pretty nice!
  • SPAs can offer dynamic designs and a “smooth” user experience. Smooth here implies that when some piece of information changes, only an element of the location is updated, versus having to reload the entire page. In fact, in case you don’t have a contemporary smartphone, SPAs won’t feel so smooth, as they have a tendency to be pretty heavy. All that JavaScript starts to pull down the performance.
  • There’s a big ecosystem of libraries and developers with experience on this stack. That is pretty circular logic: is the stack popular due to the ecosystem, or is there an ecosystem due to the popularity? Either way, this point stands.
    React was created by Meta.
  • A lot of money and energy has been thrown on the library, helping to shine and promote the product.

Unfortunately, there are some downsides of working within the MERN stack, essentially the most critical being the sheer complexity.

Traditional web development was done using the Model-View-Controller (MVC) paradigm. In MVC, the entire logic managing a user’s session is handled within the backend, on the server. Something like fetching a user’s data was done via function calls and SQL statements within the backend. The backend then serves fully built HTML and CSS to the browser, which just has to display it. Hence the name “server”.

In a SPA, this logic is handled on the user’s browser, within the frontend. SPAs should handle UI state, application state, and sometimes even server state all within the browser. API calls should be made to the backend to fetch user data. There continues to be quite a little bit of logic on the backend, mainly exposing data and functionality through APIs.

As an instance the difference, let me use the analogy of a industrial kitchen. The client will likely be the frontend and the kitchen will likely be the backend.

MVCs vs. SPAs. Image generated by ChatGPT.

Traditional MVC apps are like dining at a full-service restaurant. Yes, there’s a number of complexity (and yelling, if  is to be believed) within the backend. However the frontend experience is easy and satisfying: all the client has to do is pick up a fork and eat their food.

SPAs are like eating at a buffet-style dining restaurant. There continues to be quite a little bit of complexity within the kitchen. But now the client also has to choose what food to grab, mix them, arrange them on the plate, where to place the plate when finished, etc.

Andrej Karpathy had a tweet recently discussing his frustration with attempting to construct web apps in 2025. It will possibly be overwhelming for those latest to the space.

With a purpose to construct MVPs with AI integration rapidly, our agency has decided to forgo the SPA and as a substitute go together with the standard MVC approach. Particularly, we now have found Ruby on Rails (often denoted as Rails) to be the framework best suited to quickly developing and deploying quality apps with AI integration. Ruby on Rails was developed by David Heinemeier Hansson in 2004 and has long been often called an amazing web framework, but I might argue it has recently made leaps in its ability to include AI into apps, as we are going to see.

Django is the preferred Python web framework, and likewise has a more traditional pattern of development. Unfortunately, in our testing we found Django was simply not as full-featured or “batteries included” as Rails is. As an easy example, Django has no built-in background job system. Nearly all of our apps incorporate background jobs, so to not include this was disappointing. We also prefer how Rails emphasizes simplicity, with Rails 8 encouraging developers to easily self-host their apps as a substitute of going through a provider like Heroku. Additionally they recently released a stack of tools meant to exchange external services like Redis.

“But what concerning the smooth user experience?” you would possibly ask. The reality is that modern Rails includes several ways of crafting SPA-like experiences without the entire heavy JavaScript. The first tool is Hotwire, which bundles tools like Turbo and Stimulus. Turbo permits you to dynamically change pieces of HTML in your webpage without writing custom JavaScript. For the times where you do need to incorporate custom JavaScript, Stimulus is a minimal JavaScript framework that permits you to do exactly that. Even when you wish to use React, you possibly can achieve this with the react-rails gem. So you possibly can have your cake, and eat it too!

SPAs usually are not the one reason for the rise in complexity, nonetheless. One other has to do with the appearance of the microservices architecture.

Microservices are for Macrocompanies

Once more, we discover ourselves comparing the easy past with the complexity of today.

Up to now, software was primarily developed as monoliths. A monolithic application implies that all the several parts of your app — reminiscent of the user interface, business logic, and data handling — are developed, tested, and deployed as one single unit. The code is all typically housed in a single repo.

Working with a monolith is easy and satisfying. Running a development setup for testing purposes is straightforward. You’re working with a single database schema containing your entire tables, making queries and joins straightforward. Deployment is easy, since you only have one container to take a look at and modify.

Nevertheless, once your organization scales to the scale of a Google or Amazon, real problems begin to emerge. With tons of or hundreds of developers contributing concurrently to a single codebase, coordinating changes and managing merge conflicts becomes increasingly difficult. Deployments also change into more complex and dangerous, since even minor changes can blow up your complete application!

To administer these issues, large firms began to coalesce across the microservices architecture. This can be a sort of programming where you design your codebase as a set of small, autonomous services. Each service owns its own codebase, data storage, and deployment pipelines. As an easy example, as a substitute of stuffing your entire logic regarding an OpenAI client into your most important app, you possibly can move that logic into its own service. To call that service, you’d then typically make REST calls, as opposed to operate calls. This ups the complexity, but resolves the merge conflict and deployment issues, since each team within the organization gets to work on their very own island of code.

One other profit to using microservices is that they permit for a polyglot tech stack. Which means each team can code up their service using whatever language they like. If one team prefers JavaScript while one other likes Python, this isn’t any issue. Once we first began our agency, this concept of a polyglot stack pushed us to make use of a microservices architecture. Not because we had a big team, but because we each wanted to make use of the “best” language for every functionality. This meant:

  • Using Ruby on Rails for web development. It’s been battle-tested on this area for a long time.
  • Using Python for the AI integration, perhaps deployed with something like FastAPI. Serious AI work requires Python, I used to be led to consider.

Two different languages, each focused on its area of specialty. What could go flawed?

Unfortunately, we found the technique of development frustrating. Just organising our dev environment was time-consuming. Having to wrangle Docker compose files and manage inter-service communication made us wish we could return to the wonder and ease of the monolith. Having to make a REST call and arrange the suitable routing in FastAPI as a substitute of creating an easy function call sucked.

I believed. After which I gave it a try.

And I’m glad I did.

I discovered the technique of developing an MVP with AI integration in Ruby very satisfying. We were capable of sprint where before we were jogging. I loved the emphasis on beauty, simplicity, and developer happiness within the Ruby community. And I discovered the state of the AI ecosystem in Ruby to be surprisingly mature and improving each day.

In the event you are a Python programmer and are scared off by learning a brand new language like I used to be, let me comfort you by discussing the similarities between the Ruby and Python languages.

Ruby and Python: Two Sides of the Same Coin

I consider Python and Ruby to be like cousins. Each languages incorporate:

  • High-level Interpretation: This implies they abstract away a number of the complexity of low-level programming details, reminiscent of memory management.
  • Dynamic Typing: Neither language requires you to specify if a variable is an intfloatstring, etc. The kinds are checked at runtime.
  • Object-Oriented Programming: Each languages are object-oriented. Each support classes, inheritance, polymorphism, etc. Ruby is more “pure”, within the sense that literally all the things is an object, whereas in Python a number of things (reminiscent of if and for statements) usually are not objects.
  • Readable and Concise Syntax: Each are considered easy to learn. Either is great for a first-time learner.
  • Wide Ecosystem of Packages: Packages to do all types of cool things can be found in each languages. In Python they’re called libraries, and in Ruby they’re called gems.

The first difference between the 2 languages lies of their philosophy and design principles. Python’s core philosophy will be described as:

There needs to be one — and preferably just one — obvious option to do something.

In theory, this could emphasize simplicity, readability, and clarity. Ruby’s philosophy will be described as:

There’s all the time a couple of option to do something. Maximize developer happiness.

This was a shock to me after I converted from Python. Take a look at this straightforward example emphasizing this philosophical difference:

# A fight over philosophy: iterating over an array
# Pythonic way
for i in range(1, 6):
    print(i)

# Ruby way, option 1
(1..5).each do |i|
  puts i
end

# Ruby way, option 2
for i in 1..5
  puts i
end

# Ruby way, option 3
5.times do |i|
  puts i + 1
end

# Ruby way, option 4
(1..5).each i

One other difference between the 2 is syntax style. Python primarily uses indentation to indicate code blocks, while Ruby uses do…end or {…} blocks. Most include indentation inside Ruby blocks, but that is entirely optional. Examples of those syntactic differences will be seen within the code shown above.

There are a number of other little differences to learn. For instance, in Python string interpolation is completed using f-strings: f"Hello, {name}!", while in Ruby they’re done using hashtags: "Hello, #{name}!". Inside a number of months, I feel any competent Python programmer can transfer their proficiency over to Ruby.

Recent AI-based Gems

Despite not being within the conversation when discussing AI, Ruby has had some recent advancements on this planet of gems. I’ll highlight among the most impressive recent releases that we now have been using in our agency to construct AI apps:

RubyLLM (link) — Any GitHub repo that gets greater than 2k stars inside a number of weeks of release deserves a mention, and RubyLLM is unquestionably worthy. I even have used many clunky implementations of LLM providers from libraries like LangChain and LlamaIndex, so using RubyLLM was like a breath of fresh air. As an easy example, let’s take a have a look at a tutorial demonstrating multi-turn conversations:

require 'ruby_llm'

# Create a model and provides it instructions
chat = RubyLLM.chat
chat.with_instructions "You're a friendly Ruby expert who loves to assist beginners."

# Multi-turn conversation
chat.ask "Hi! What does attr_reader do in Ruby?"
# => "Ruby creates a getter method for every symbol...

# Stream responses in real time
chat.ask "Could you give me a brief example?" do |chunk|
  print chunk.content
end
# => "Sure!  
#     ```ruby
#     class Person
#       attr...

Simply amazing. Multi-turn conversations are handled robotically for you. Streaming is a breeze. Compare this to the same implementation in LangChain:

from langchain_openai import ChatOpenAI
from langchain_core.schema import SystemMessage, HumanMessage, AIMessage
from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

SYSTEM_PROMPT = "You're a friendly Ruby expert who loves to assist beginners."
chat = ChatOpenAI(streaming=True, callbacks=[StreamingStdOutCallbackHandler()])

history = [SystemMessage(content=SYSTEM_PROMPT)]

def ask(user_text: str) -> None:
    """Stream the reply token-by-token and keep the context in memory."""
    history.append(HumanMessage(content=user_text))
    # .stream yields message chunks as they arrive
    for chunk in chat.stream(history):
        print(chunk.content, end="", flush=True)
    print()                                          # newline after the reply
    # the ultimate chunk has the total message content
    history.append(AIMessage(content=chunk.content))

ask("Hi! What does attr_reader do in Ruby?")
ask("Great - could you show a brief example with attr_accessor?")

Yikes. And it’s vital to notice that this can be a grug implementation. Wish to know the way LangChain really expects you to administer memory? Check out these links, but grab a bucket first; chances are you’ll get sick.

Neighbors (link) — This is a wonderful library to make use of for nearest-neighbors search in a Rails application. Very useful in a RAG setup. It integrates with Postgres, SQLite, MySQL, MariaDB, and more. It was written by Andrew Kane, the identical guy who wrote the pgvector extension that enables Postgres to behave as a vector database.

Async (link) — This gem had its first official release back in December 2024, and it has been making waves within the Ruby community. Async is a fiber-based framework for Ruby that runs non-blocking I/O tasks concurrently while letting you write easy, sequential code. Fibers are like mini-threads that every have their very own mini call stack. While not strictly a gem for AI, it has helped us create features like web scrapers that run blazingly fast across hundreds of pages. Now we have also used it to handle streaming of chunks from LLMs.

Torch.rb (link) — In the event you are all in favour of training deep learning models, then surely you may have heard of PyTorch. Well, PyTorch is built on LibTorch, which essentially has a number of C/C++ code under the hood to perform ML operations quickly. Andrew Kane took LibTorch and made a Ruby adapter over it to create Torch.rb, essentially a Ruby version of PyTorch. Andrew Kane has been a hero within the Ruby AI world, authoring dozens of ML gems for Ruby.

Summary

In brief: constructing an online application with AI integration quickly and cheaply requires a monolithic architecture. A monolith demands a monolingual application, which is crucial in case your end goal is quality apps delivered with speed. Your most important options are either Python or Ruby. In the event you go together with Python, you will likely use Django on your web framework. In the event you go together with Ruby, you will likely be using Ruby on Rails. At our agency, we found Django’s lack of features disappointing. Rails has impressed us with its feature set and emphasis on simplicity. We were thrilled to search out almost no issues on the AI side.

In fact, there are occasions where you won’t need to use Ruby. In the event you are conducting research in AI or training machine learning models from scratch, you then will likely need to keep on with Python. Research almost never involves constructing Web Applications. At most you’ll construct an easy interface or dashboard in a notebook, but nothing production-ready. You’ll likely want the newest PyTorch updates to make sure your training runs quickly. You might even dive into low-level C/C++ programming to squeeze as much performance as you possibly can out of your hardware. Perhaps you’ll even try your hand at Mojo.

But in case your goal is to integrate the newest LLMs — either open or closed source — into web applications, then we consider Ruby to be the far superior option. Give it a shot yourselves!

Partly three of this series, I’ll dive right into a fun experiment: just how easy can we make an online application with AI integration? Stay tuned.

🔥

ASK ANA

What are your thoughts on this topic?
Let us know in the comments below.

0 0 votes
Article Rating
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Share this article

Recent posts

0
Would love your thoughts, please comment.x
()
x