RubyLLM: A delightful Ruby way to work with AI
github.com
github.com
Aside from that the DSL is quite excellent.
# Stream responses in real-time
chat.ask "Tell me a story about a Ruby programmer" do |chunk|
print chunk.content
endConcurrency support is missing from the language syntax and this particular library as a concept. This is by design, to not distract from beautiful code. Your request will make zero progress and take up memory while waiting for the LLM answer. Other threads might make progress on other requests, but in real world deployments this will be a handful (<10). This server will get 10s of requests per second when something written in JS or Go will get many 1000s.
It’s amazing how the Ruby community argues against their own docs and doesn’t acknowledge the design choices their language creators have made.
Checkout the async gem, including async-http, async-websockets, and the Falcon web server.
Valid point. I'm actually already working on testing better streaming using async-http-faraday, which configures the default adapter to use async_http with falcon and async-job instead of thread-based approaches like puma and SolidQueue. This should significantly improve resource efficiency for AI workloads in Ruby - something I'm not aware is implemented by other major Ruby LLM libraries. The current approach with blocks is idiomatic Ruby, but the upcoming async support will make the library even better for production use cases. Stay tuned!
What I advise (and aim for) is only pulling out the sharp knives for "library" code, but application code should stay "simple" (and this much more easily navigable). Otherwise you can absolutely make a bloody mess!
Even a goto can be elegant sometimes.
If you have good reason to believe that an app will stick around for more than a year, be maintained by more than 3 people, or grow to more than 500k lines of code (sub in whatever metrics make sense to you), don't put off removing global state for later. You will regret it eventually, and it doesn't cost much to do it right the first time.
(Also, no mainstream language I'm aware of forces you to not use global state. Even Java, famed for its rigidity, has global state readily available if you really do need it.)
In a project with more than a few people on it, your architecture will decay if it can decay. Avoiding global state removes one major source of potential decay.
No, it isn't—I'm the one who inserted the word "almost" into that sentence! Where did you get the idea that I meant always/never?
Like I said, you can point to exceptions but that doesn't change the rule. It's better to teach the rule and break it when you really know what you're doing—when you understand that you're breaking a rule and can articulate why you need to and why it's okay this time—than it is to spread the idea that globals are really just fine and you need to weigh the trade-offs. The odds are strongly against you being the exception, and you should act accordingly, not treat globals as just another tool.
Sometimes amputation is the right move to save someone's life, but you certainly should not default to that for every papercut. It's a tool that comes out in extreme circumstances only when a surgeon can thoroughly justify it.
But I’m also aware that you’re comparing using global state to amputating a human limb. I don’t think it’s nearly that extreme. I certainly wouldn’t say global state “almost always leads to bad architecture,” as evidenced by my aligning with a framework which has a whole construct for globals baked into it (Rails’ Current singleton) that I happen to enjoy using.
Sure, global state is a sharp knife, which I already said. It can inflict pain, but it’s also a very useful tool in certain scenarios (more than would equate to “almost [never]” IMO).
So your response aligns with how I took your original post, and what I inferred “almost” really meant: basically never. My point is that I don’t agree with your take being a “rule.” While I understand your perspective, instead of saying basically never, I would say, “it depends.”
though Ruby's expressiveness comes at a cost and I'd personally stick with Go in a team but use something like RubyLLM for personal projects
Ruby and go each prioritize different parts of the developer experience. Ruby prioritizes the experience of the author of the initial code at the expense of the experience of the maintainer who comes later. Go prioritizes the experience of a maintainer over the experience of the initial author. Both prioritize a developer, both de-prioritize a different developer, and which one makes more sense really depends on the ratio of time spent on writing greenfield code versus maintaining something another human wrote years ago who's long gone.
I find changes in existing Go software often end up spreading far deeper into the app than you'd expect.
The runtime is fantastic, though, so I don't see it losing it's popularity anytime soon.
That’s kind of just the surface level of maintenance though. Go is not so much focused on making it easy to read a single file, but on minimizing the chains of abstraction and indirection you need to follow to understand exactly how things work.
It’s much more likely that all the logic and config to do something is right there in that file, or else just one or two “Go to definition” clicks away. You end up with way more boilerplate and repetition, but also looser coupling between files, functions, and components.
Contrast that to a beautiful DSL in Ruby. It’s lovely until it breaks or you need to extend it, and you realize that a small change will require refactoring call sites across a dozen different files. Oh and now this other thing that reused that logic is broken, and we’ve got to update most of the test suite, and so on.
This is the biggest part of it: maintainers need static analysis and/or (preferably and) very good grepability to help them navigate foreign code. Ruby by its nature makes static analysis essentially impossible to do consistently, whereas Go leans to the opposite extreme.
// Must[T](T, error) T is necessary because of Go error handling differences
chat := Must(gollm.Chat().WithModel("claude-3-7-sonnet-20250219"))
resp := Must(chat.Ask("What's the difference between an unexported and an exported struct field?"))
resp = Must(chat.Ask("Could you give me an example?"))
resp = Must(chat.Ask("Tell me a story about a Go programmer"))
for chunk := range resp { // Requires Go 1.23+ for iterators
fmt.Print(chunk.Content)
}
resp = Must(chat.WithImages("diagram1.png", "diagram2.png").Ask("Compare these diagrams"))
type Search struct {
Query string `description:"The search query" required:"true"`
Limit int `description:"Max results" default:"5"`
}
func (s Search) Execute() ([]string, error) { ... }
resp = Must(chat.WithTool[Search]().Ask("Find documents about Go 1.23 features"))
And so on. Syntax is different, of course, but semantics (save for language-specific nuances, like error handling and lack of optional arguments) are approximately the same, biggest difference being WithSomething() having to precede Ask()Go is more about making it easier to build fast and robust systems. But it really doesn't care if the code itself is ugly and full of boilerplate.
As I've gotten more experience, I've come to really appreciate Go's tradeoffs. It's not as fun up front, but on the other hand, you're less likely to get server alerts at 4am. It really depends what you're building though.
My favorite example of this is the amazing useful and amazing whack Ruby array arithmetic; subtraction (`arr1 - arr2`) is element-wise removal, but addition (`arr1 + arr2`) is a simple append. These are almost always exactly what you want to do when you reach for them, but they're completely "incorrect" mathematically.
I thought it was Python and readability and "one way of doing things".
Langchain really, I think, pushed the LLM makers forward toward adding those features but unfortunately it got left in the dust and became somewhat of a zombie. Simultaneously, the foundational LLM providers kept adding things to turn them more into a walled garden, where you no longer needed to connect multiple things (like scraping websites with one tool, feeding that into the LLM, then storing in a vector datastore - now that's all built in).
I think Langchain has tried to pivot (more than once perhaps) but had they not taken investor $$ early on (and good for them) I suspect that it would have just dried up and the core team would have gone on to work at OpenAI, Anthropic, etc.
It doesn't deal with any of the hard problems you'll routine face with implementation.
I'm pretty sure this API semantics (instance builder to configure, and then it's ask/paint/embed with language-native way to handle streaming and declarative tools) would look beautiful and easy to use in many other languages, e.g. I can imagine a similar API - save, of course, for the Rails stuff - in Python, C# or Erlang. While this level of API may be not perfectly sufficient for all possible LLM use cases, it should certainly speed up development time when this level of API is all that's possible needed.
If you see the typescript options it's like giving yourself a water boarding session through your own volition.
// Just ask questions
const chat: Chat = LLM.chat;
chat.ask("What's the best way to learn TypeScript?");
// Analyze images
chat.ask("What's in this image?", { image: "ts_conf.jpg" });
// Generate images
LLM.paint("a sunset over mountains in watercolor style");
// Create vector embeddings
LLM.embed("TypeScript is powerful and scalable");
// Let AI use your code
class Calculator {
description = "Performs calculations";
params = {
expression: { type: "string", desc: "Math expression to evaluate" },
};
execute(args: { expression: string }): string {
return eval(args.expression).toString();
}
}
chat.withTool(new Calculator()).ask("What's 123 * 456?");Also:
const chat: Chat = LLM.chat;
...is not instantiating a class, where Ruby is doing so behind the scenes. You'd need yet another pair of parens to make a factory!This is mainly a matter of syntactic style!
chat = RubyLLM.chat
Is ambiguous, though. You can't know if it's an assignment or creating a new object. I don't think that's more readable.I always thought such minor syntactic differences are unimportant, except for the folks who still learn syntax and haven't seen too many languages out there to stop caring much about it.
YMMV of course, but whenever I need to jump hoops with some API or have things conveniently returned to me in a single call matters a lot for my developer happiness. Whenever my code needs semicolons or indentation or parens feels such a negligibly tiny nuance to me but things like this don't even blip on my mental radar... I always think about what the code does, and don't even see those details (unless I have a typo lol).
Maybe my opinion on this is just the echoes from the ancient C vs Pascal vs BASIC syntax holy wars while I was still a schoolkid, idk. I mean, when I wrote Scheme or Lisp I haven't really "seen" all those parentheses (but then, I just checked some Lisp code and syntax looks off and takes time to get through, since I haven't practiced it in a long while and it's pretty different from anything I've used any recently).
Again, YMMV, but `const chat = new LLM.Chat();` and `chat = RubyLLM.chat` are exactly the same thing to me - I don't remember actual tokens from the screen, I immediately mentally process those both as something like "instantiate a chat object and assign `chat` to it" (without really verbalizing it much, but as an concept/idea). And I don't think a little syntactic noise like `const` or `;` is making things worse or better for me. Although, to be fair, I could be wrong here - I haven't really did any experiments in this regards, with properly defined methodology and metrics, and my subjective perception could be deceptive. Sadly, I'm no scientist and not even sure how to set up one correctly...
Ruby: late to the party, brought a keg.
Keep going! Happy to see ollama support PR in draft.
chat.ask "What's being said?", with: { audio: "meeting.wav" }
definitely don't want users to get a valid response only 75% of the times, maybe?If you don’t like it, don’t use it.
I agree that waiting for response can be an issue. I don't think this is meant to be for such purposes, but for tools that would process and create artifacts based on inputs.
I love Mistral and local LLMs, so this would probably the thing I would like to add.
https://news.ycombinator.com/item?id=43369977
But it seems hashnode.dev as a domain is blocked entirely. Hopefully Ruby gets another chance in AI era.
Love this project!
haven't tried it yet though
`Document.search(query).limit(limit).map(&:title)` how do you defined the documents to search on?
Allowing ai to eval() code or execute any sql statement would scare the crap outta me personally.
You’re totally right that eval()’ing unknown code is terrible but it doesn’t look like the gem itself is doing that.
The usage of eval() is in a user written tool in the docs. Definitely a bd example and should probably be changed
For us, it made sense to leverage one of the best domain modeling and ORM frameworks out there. Most of our inference is http calls to foundational models, but we can still fine tune and host models on GPUs using Python.
Inference matters, but part of building an effective user platform are the same old SaaS problems we’ve had before, and Rails just works. Inbound and outbound email done in a day. Turning an OCR’d title from ALL CAPS into Title Case is one method call and not a whole custom algorithm, etc.
A lot of people seem to think Ruby is slow for some reason but it’s as fast as Python, and with falcon as fast as node for async behavior. Safe to say the application language taking 0.03 seconds instead of 0.003 seconds when you have to wait 3 seconds for first token is absolutely not the bottleneck with LLM heavy workflows, anyway.
And yes, metaprogramming is a powerful tool with which you can easily shoot yourself in the foot. We culturally just don’t write any code that’s not greppable so don’t use method_missing kinds of things unless it’s in a robust gem like active record. Pretty trivial problem to solve really.
PS - We’re hiring, if that philosophy aligns with you!
Now compare that to my recent experience with having Cursor help me work on a preexisting Node/React app...geez. What a pain. (It doesn't help that I wasn't the one that originally created the React app though.)
I played with Ruby when I was a teenager (~2015 or so), and I definitely remember enjoying it. I know there's still a vocal group of users who love it, so I would be interested in digging in again.
https://guides.rubyonrails.org/getting_started.htm
Just have a toy app you want to build in mind
It's my favorite programming language but I seldom get to use it because I'm an AI Engineer. But I just recently went out on my own so I guess that can change now, hm...
Outside of it, you might find interesting libraries like sinatra, sequel, roda, dryrb, faraday, sorbet, truffle ruby…
Don’t let it bum you out.
It is somewhat objectively true:
https://octoverse.github.com/2022/top-programming-languages
https://github.blog/wp-content/uploads/2024/10/GitHub-Octove...
It doesn't mean much, and this library can be reproduced in any of those top 10 languages from what I can tell.
It ranks right after Shell (#8) and C (#9). Ruby is still a mainstream language, and it's fairly easy to find a Ruby job. Compare that to Clojure or Haskell.
Of the many developers who used to write Ruby (myself included), I would wager not many of those same people still do.
Keep in mind the number of developers overall is rising rapidly still.
The ratio of people who can code in Python or Ruby to people who can code in Rust or C++ is very high.
It's easy to forget though that number of lines of code required to do something is also a valid metric and Ruby beats Rust on that.
So if you're shipping CRUD web apps that might be a more important metric than say memory usage or CPU time.
Different job, different tool. More people want to ship web apps than write their own JITs.
Engineering is the art of trade offs.
Ruby code can be downright poetic, for better or worse. There's a certain kind of magic to the kind of code it enables. That's not always good, but it _is_ beautiful.
I encourage everybody to read the venerable "Why's Poignant Guide to Ruby" [1] to see what I'm talking about.
I wish Ruby was cross-platform. It still only works on Windows using the MSYS2 emulation layer, and the only reason as far as I can tell is that it committed hard and early to `fork()` as the main way to use multiple cores.
You'll often see the same names coming back on every post to angrily insist that no one is interested in Ruby
...apart from them obviously because if they didn't they would be busy trolling something else. :P
I don't think so. I mean there are complains about stuff you care about like people complaining about Healthcare. (edit: there are other forms of caring, see my grandchild comment)
Dissing on Ruby is definitely not this, they are not Ruby users wanting Ruby to be better. They don't even know Ruby apart from dissing on it is socially accepted, and makes them feel good.
Surely a one-off comment about nobody using Ruby doesn’t mean you “care”, but if it is true that it is the same people who keep commenting, they obviously care.
Is it because they are jealous of the beauty of Ruby/Rails, as a Rubyist I’d think so, but who is to say really. Maybe they worked at a company where they replaced whatever their favourite stack is with Rails and they have hated Ruby ever since. It could be anything.
You wouldn’t keep responding to stuff you don’t care about at some level.
All in all, I don't think that other forms of caring apply either. I think that parroting "Ruby is dead" doesn't mean that they care about Ruby, it's just a thing people like to parrot, without the meaning realizing in their heads. A form of bonding, a form of distraction, a form of opening to a social interaction, a form of self-reassurance etc. It is lot of things, and caring about Ruby at all is usually not among those (IMO).
I agree. Parroting some meme isn’t caring per se. But I was working under the assumption that the statement that it was the same names who keep doing it. If you say “Ruby is dead” 5 times a year it isn’t necessarily “caring” if it becomes 100 times a year there is something else at play.
In contrast I have also worked on VSCode which is similarly huge but written in Typescript. Faaaar easier to work with, enough that I've been able to contribute a couple of medium sized features and several bug fixes.
So when people say "yeay Ruby" I try to discourage them because I don't want more Ruby code in the world that I might have the misfortune of having to interact with in future.
Now that they use something "far easier to work with", the UX gets to suffer accordingly.
I've never been in a situation where making the customer happy was synonymous with applying best practices to the tech stack or otherwise making it so everyone and their dog can contribute.
It all seems pretty negative value to me though it’s terribly slow.
My experience is that the sort of folks who misuse Ruby's powerful features are the sort of idiotes who dont realise that because a thing can be done, doesn't mean that it should be done. These are the sort of people who are capable of misusing most languages.
I'm having a hard time thinking that ruby is difficult to understand, particularly compared to its opposites lisp, erlang, Haskell, e.g. languages that are extremely simple to the point where the burden of complexity is shoved into code space.
I think so. I'm not an expert but the Gitlab codebase seems like fairly typical Ruby to me.
chat = python_llm.Chat()
_ = chat.ask"What's the best way to learn Python?")
# Analyze images
_ = chat.ask("What's in this image?", image="python_conf.jpg")
# Generate images
_ = python_llm.paint("a sunset over mountains in watercolor style")
# Stream responses
for chunk in chat.ask("Tell me a story about a Python programmer"):
print(chunk.content)
# Can be a class if necessary, but for this weather thingy we can probably do with a simple callable
# Requires Python 3.9+ for typing.Annotated
def get_weather(
latitude: Annotated[Decimal, "Latitude of the location"],
longitude: Annotated[Decimal, "Longitude of the location"]
) -> str:
"""
Gets current weather for a location.
"""
...
_ = chat.with_tool(get_weather).ask("What's the weather in Berlin? (52.5200, 13.4050)")
(The `_ =` bits are mine, to emphasize we have a meaningful result and we're knowingly and willingly discarding it. Just a habit, I hope it doesn't bug people.)Ruby has significantly more capable metaprogramming facilities, but they aren't used in RubyLLM, it's all just objects and methods (biggest difference being use of iterable in Python vs providing a block in Ruby, as I felt an iterable would be more Pythonic here), which is nothing Ruby-specific.
And IMHO advanced metaprogramming should be used carefully, as it may make code pretty but really hard to comprehend and analyze. My largest issue with Rails is difficulty to tell where things are coming from and what's available (lack of implicit imports and ability to re-open any class or module and inject more stuff in there so there's no single place that defines it is a double-edged sword that may lead to chaos if wielded carelessly - YMMV, of course, I'm merely stating my personal preferences here).
> 166 points|ksec|4 days ago|21 comments
> 168 points by ksec 4 hours ago
But yes; it’s a bit dodgy to resurface old news like this imo and pretend it’s new news.
I’d go as far as to say that being at #1, under the circumstances, means it’s been artificially boosted somehow.
I haven’t the foggiest why anyone would bother though.
1. Moderators can re-submit interesting stories from a second chance pool https://news.ycombinator.com/pool (This might happen automatically from time to time?) When this happens some of the timestamps get updated but others dont.
2. Moderators can invite users via email to re-submit stories. https://news.ycombinator.com/invited
module RubyLLM
class Error < StandardError; end
class << self
def chat(model: nil)
Chat.new(model: model)
end
https://github.com/crmne/ruby_llm/blob/9825f4fea089b1b974961...If you're wondering about module RubyLLM. That's just how Ruby is often written.
Addendum: Ruby does not require you to put the opening and closing parenthesis on a function to run that function, and it's not always put there when you have zero or 1 parameter (I find it to be cleaner when you have a parameter, but have no opinion when there isn't a parameter)
In the example code from the link itself, you'll see:
chat.ask "What's the best way to learn Ruby?"
which is the same as chat.ask("What's the best way to learn Ruby?")mind = blown
I always liked how functionName denotes the function and functionName() calls the function, and then it denotes the result e.g. in JavaScript or in math. But just saying functionName to call a function makes the code read more like English. Code that reads like English > code that reads like math. (And you can still talk about functions of course.)
https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-rele...
They need fundamental breaking changes to the language to fix this, which means people won’t be able to use their beloved pile of 438 gems that haven’t seen a commit in 7 years. If I had to bet, I’d say the language is dead. It might still be a nice niche language for easy prototyping, but the world has moved on to async/await (js/python/Rust/C++) or stackful coroutines (Go).