Skip to content

Rails Generator for RubyLLM Models #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
487d4e4
feat: add Rails generator for RubyLLM models
kieranklaassen Mar 27, 2025
3577f31
test: add generator template tests and fix zeitwerk warning
kieranklaassen Mar 27, 2025
24467aa
feat: add cross-database support for JSON columns
kieranklaassen Mar 27, 2025
2d50909
fix: ensure proper migration order for database schema
kieranklaassen Mar 27, 2025
6e68d36
Add README template for generator output
kieranklaassen Mar 27, 2025
dacb148
Remove unnecessary comments from create_tool_calls_migration.rb
kieranklaassen Mar 27, 2025
f469f60
Update generator templates to use .tt extension
kieranklaassen Mar 27, 2025
844614b
Improve test organization and fix style issues
kieranklaassen Mar 27, 2025
131e51c
chore: memory
kieranklaassen Mar 27, 2025
f40d1b4
Delete CLAUDE.md
kieranklaassen Mar 27, 2025
5eeedf9
Update CONTRIBUTING.md
kieranklaassen Mar 28, 2025
9d8c2d6
chore: clean up whitespace and add install generator tests
kieranklaassen Mar 28, 2025
61d66ab
first run at options for model names
jamster Mar 28, 2025
a239b34
fixing generator specs with updated `options` setup
jamster Mar 31, 2025
8316ce5
Merge pull request #1 from jamster/generators
kieranklaassen Mar 31, 2025
cf4a560
Merge branch 'main' into generators
kieranklaassen Apr 1, 2025
492188f
Merge branch 'main' into generators
crmne Apr 2, 2025
52d761a
Merge branch 'main' into generators
kieranklaassen Apr 2, 2025
cc3d23f
Addresses comments in PR
jamster Apr 8, 2025
9eba7bc
Merge pull request #2 from jamster/generators
kieranklaassen Apr 10, 2025
7cded01
Merge branch 'main' into generators
kieranklaassen Apr 10, 2025
21c4ee0
Merge branch 'main' into generators
kieranklaassen Apr 18, 2025
fda4df1
docs
kieranklaassen Apr 18, 2025
6270f22
docs: Update Rails integration guide to include generator usage and i…
kieranklaassen Apr 18, 2025
f5454aa
docs: Update Rails integration guide with user association in chat re…
kieranklaassen Apr 18, 2025
5be36c1
docs: Add ChatStreamJob implementation to Rails integration guide
kieranklaassen Apr 18, 2025
1f8d3b0
Merge branch 'main' into generators
kieranklaassen Apr 21, 2025
a350738
Merge branch 'main' into generators
crmne Apr 23, 2025
1ed3c2e
Address PR comments: rename README.md.tt, fix migration order, update…
kieranklaassen Apr 29, 2025
f16858a
Delete .cursor/rules/add_new_provider.mdc
kieranklaassen Apr 29, 2025
26bcf50
Delete .cursor/rules/ruby_llm_conventions.mdc
kieranklaassen Apr 29, 2025
c352d5f
Delete .cursor/rules/ruby_style_guide.mdc
kieranklaassen Apr 29, 2025
b064713
Delete .cursor/rules/ruby_llm_philosophy.mdc
kieranklaassen Apr 29, 2025
5a9b3b8
fix: address PR comments - remove sleep calls and fix formatting
kieranklaassen Apr 29, 2025
224b24e
fix: revert rails.md to previous version as requested in PR comments
kieranklaassen Apr 29, 2025
1fe4320
fix: remove empty line in rails.md as requested in PR comments
kieranklaassen Apr 29, 2025
882f3fc
fix: revert rails.md to original state as requested in PR review
kieranklaassen Jun 14, 2025
4789f80
test: update generator specs to match INSTALL_INFO rename
kieranklaassen Jun 14, 2025
d2196bd
Merge branch 'main' of https://github.com/crmne/ruby_llm into generators
kieranklaassen Jun 14, 2025
bf2244a
docs: update Rails guide to include quick setup instructions with gen…
kieranklaassen Jun 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,4 @@ Gem versioning follows [Semantic Versioning](https://semver.org/):

Releases are handled by the maintainers through the CI/CD pipeline.

Thanks for helping make RubyLLM better!
Thanks for helping make RubyLLM better!
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,37 @@ chat.with_model('gemini-2.0-flash').ask "What's your favorite algorithm?"

## Rails integration that makes sense

### Option 1: Create new models with the generator

Simply run the generator to set up all required models:

```bash
rails generate ruby_llm:install
```

This creates all necessary migrations, models, and database tables. Then just use them:

```ruby
# In your controller
chat = Chat.create!(model_id: "gpt-4o-mini")
chat.ask("What's your favorite Ruby gem?") do |chunk|
Turbo::StreamsChannel.broadcast_append_to(
chat, target: "response", partial: "messages/chunk", locals: { chunk: chunk }
)
end

# That's it - chat history is automatically saved
```

### Option 2: Add to your existing models

Or, add RubyLLM to your existing ActiveRecord models:

```ruby
# app/models/chat.rb
class Chat < ApplicationRecord
acts_as_chat

# Works great with Turbo
broadcasts_to ->(chat) { "chat_#{chat.id}" }
end
Expand Down
29 changes: 26 additions & 3 deletions docs/guides/rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,32 @@ RubyLLM provides seamless integration with Rails through ActiveRecord models. Th

## Setup

### 1. Create Migrations
### Using the Generator (Recommended)

First, create the necessary tables in your database:
The easiest way to set up RubyLLM with Rails is to use the built-in generator:

```bash
rails generate ruby_llm:install
```

This will automatically:
1. Create the necessary migrations for chats, messages, and tool calls
2. Create model files with appropriate `acts_as_*` methods
3. Set up proper relationships between models

After running the generator, simply run the migrations:

```bash
rails db:migrate
```

### Manual Setup

If you prefer to set up manually or need to customize the implementation, follow these steps:

#### 1. Create Migrations

Create the necessary tables in your database:

```ruby
# db/migrate/YYYYMMDDHHMMSS_create_chats.rb
Expand Down Expand Up @@ -65,7 +88,7 @@ Run the migrations:
rails db:migrate
```

### 2. Set Up Models
#### 2. Set Up Models

Create the model classes:

Expand Down
68 changes: 68 additions & 0 deletions lib/generators/ruby_llm/install/templates/README.md.tt
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be annoying to have a new README? I would delete this

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name "README" is a bit misleading here. This is the text generated AFTER you run the generator to explain what you did, whereas the readme for RubyLLM remains the same and untouched. Originally, this did overwrite the README, but has been corrected to be more of an ephemeral display post install.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can see it in the test added too. It's to give direction after using the generator.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# RubyLLM Rails Setup Complete!

Thanks for installing RubyLLM in your Rails application. Here's what was created:

## Models

- `<%= options[:chat_model_name] %>` - Stores chat sessions and their associated model ID
- `<%= options[:message_model_name] %>` - Stores individual messages in a chat
- `<%= options[:tool_call_model_name] %>` - Stores tool calls made by language models

## Configuration Options

The generator supports the following options to customize model names:

```bash
rails generate ruby_llm:install \
--chat-model-name=Conversation \
--message-model-name=ChatMessage \
--tool-call-model-name=FunctionCall
```

This is useful when you need to avoid namespace collisions with existing models in your application. Table names will be automatically derived from the model names following Rails conventions.

## Next Steps

1. **Run migrations:**
```bash
rails db:migrate
```

2. **Set your API keys** in `config/initializers/ruby_llm.rb` or using environment variables:
```ruby
# config/initializers/ruby_llm.rb
RubyLLM.configure do |config|
config.openai_api_key = ENV["OPENAI_API_KEY"]
config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
# etc.
end
```

3. **Start using RubyLLM in your code:**
```ruby
# Create a new chat
chat = <%= options[:chat_model_name] %>.create!(model_id: "gpt-4o-mini")

# Ask a question
response = chat.ask("What's the best Ruby web framework?")

# Get chat history
chat.messages
```

4. **For streaming responses** with ActionCable or Turbo:
```ruby
chat.ask("Tell me about Ruby on Rails") do |chunk|
Turbo::StreamsChannel.broadcast_append_to(
chat, target: "response", partial: "messages/chunk", locals: { chunk: chunk }
)
end
```

## Advanced Usage

- Add more fields to your models as needed
- Customize the views to match your application design
- Create a controller for chat interactions

For more information, visit the [RubyLLM Documentation](https://github.com/crmne/ruby_llm)
3 changes: 3 additions & 0 deletions lib/generators/ruby_llm/install/templates/chat_model.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class <%= options[:chat_model_name] %> < ApplicationRecord
<%= acts_as_chat_declaration %>
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= options[:chat_model_name].tableize %> do |t|
t.string :model_id
t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This migration must be run AFTER create_<%= options[:chat_model_name].tableize %> and create_<%= options[:tool_call_model_name].tableize %> migrations
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to enforce this constraint with the timestamp of the migration? Disclaimer: I'm new to making templates for migrations.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments on latest PR below

# to ensure proper foreign key references
class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= options[:message_model_name].tableize %> do |t|
t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
t.string :role
t.text :content
t.string :model_id
t.integer :input_tokens
t.integer :output_tokens
t.references :<%= options[:tool_call_model_name].tableize.singularize %>
t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
class Create<%= options[:tool_call_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= options[:tool_call_model_name].tableize %> do |t|
t.references :<%= options[:message_model_name].tableize.singularize %>, null: false, foreign_key: true
t.string :tool_call_id, null: false
t.string :name, null: false
t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
t.timestamps
end

add_index :<%= options[:tool_call_model_name].tableize %>, :tool_call_id
end
end
14 changes: 14 additions & 0 deletions lib/generators/ruby_llm/install/templates/initializer.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# RubyLLM configuration
RubyLLM.configure do |config|
# Set your API keys here or use environment variables
# config.openai_api_key = ENV["OPENAI_API_KEY"]
# config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
# config.gemini_api_key = ENV["GEMINI_API_KEY"]
# config.deepseek_api_key = ENV["DEEPSEEK_API_KEY"]

# Uncomment to set a default model
# config.default_model = "gpt-4o-mini"

# Uncomment to set default options
# config.default_options = { temperature: 0.7 }
end
3 changes: 3 additions & 0 deletions lib/generators/ruby_llm/install/templates/message_model.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class <%= options[:message_model_name] %> < ApplicationRecord
<%= acts_as_message_declaration %>
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class <%= options[:tool_call_model_name] %> < ApplicationRecord
<%= acts_as_tool_call_declaration %>
end
108 changes: 108 additions & 0 deletions lib/generators/ruby_llm/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# frozen_string_literal: true

require 'rails/generators'
require 'rails/generators/active_record'

module RubyLLM
# Generator for RubyLLM Rails models and migrations
class InstallGenerator < Rails::Generators::Base
include Rails::Generators::Migration
namespace "ruby_llm:install"

source_root File.expand_path('install/templates', __dir__)

class_option :chat_model_name, type: :string, default: 'Chat',
desc: 'Name of the Chat model class'
class_option :message_model_name, type: :string, default: 'Message',
desc: 'Name of the Message model class'
class_option :tool_call_model_name, type: :string, default: 'ToolCall',
desc: 'Name of the ToolCall model class'

desc 'Creates model files for Chat, Message, and ToolCall, and creates migrations for RubyLLM Rails integration'

def self.next_migration_number(dirname)
::ActiveRecord::Generators::Base.next_migration_number(dirname)
end

def migration_version
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
end

def postgresql?
ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
rescue StandardError
false
end

def acts_as_chat_declaration
acts_as_chat_params = []
if options[:message_model_name]
acts_as_chat_params << "message_class: \"#{options[:message_model_name]}\""
end
if options[:tool_call_model_name]
acts_as_chat_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
end
if acts_as_chat_params.any?
"acts_as_chat #{acts_as_chat_params.join(', ')}"
else
"acts_as_chat"
end
end

def acts_as_message_declaration
acts_as_message_params = []
if options[:chat_model_name]
acts_as_message_params << "chat_class: \"#{options[:chat_model_name]}\""
end
if options[:tool_call_model_name]
acts_as_message_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
end
if acts_as_message_params.any?
"acts_as_message #{acts_as_message_params.join(', ')}"
else
"acts_as_message"
end
end

def acts_as_tool_call_declaration
acts_as_tool_call_params = []
if options[:message_model_name]
acts_as_tool_call_params << "message_class: \"#{options[:message_model_name]}\""
end
if acts_as_tool_call_params.any?
"acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
else
"acts_as_tool_call"
end
end

def create_migration_files
# Use a fixed timestamp for testing and to ensure they're sequential
# @migration_number = Time.now.utc.strftime('%Y%m%d%H%M%S')
migration_template 'create_chats_migration.rb.tt', "db/migrate/create_#{options[:chat_model_name].tableize}.rb"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah there you go that's where we can enforce the order.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kieranklaassen can you enforce the order by using timestamps in the name just like rails does?


# Increment timestamp for the next migration
# @migration_number = (@migration_number.to_i + 1).to_s
migration_template 'create_messages_migration.rb.tt', "db/migrate/create_#{options[:message_model_name].tableize}.rb"

# Increment timestamp again for the final migration
# @migration_number = (@migration_number.to_i + 2).to_s
migration_template 'create_tool_calls_migration.rb.tt', "db/migrate/create_#{options[:tool_call_model_name].tableize}.rb"
end

def create_model_files
template 'chat_model.rb.tt', "app/models/#{options[:chat_model_name].underscore}.rb"
template 'message_model.rb.tt', "app/models/#{options[:message_model_name].underscore}.rb"
template 'tool_call_model.rb.tt', "app/models/#{options[:tool_call_model_name].underscore}.rb"
end

def create_initializer
template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
end

def show_readme
content = ERB.new(File.read(source_paths.first + '/README.md.tt')).result(binding)
say content
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
loader.ignore("#{__dir__}/tasks")
loader.ignore("#{__dir__}/ruby_llm/railtie")
loader.ignore("#{__dir__}/ruby_llm/active_record")
loader.ignore("#{__dir__}/generators")
loader.setup

# A delightful Ruby interface to modern AI language models.
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_llm/active_record/acts_as.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall') # rubocop:d
@chat_class = chat_class.to_s
@tool_call_class = tool_call_class.to_s

belongs_to :chat, class_name: @chat_class
belongs_to :chat, class_name: @chat_class, foreign_key: "#{@chat_class.underscore}_id"
has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy

belongs_to :parent_tool_call,
Expand All @@ -52,7 +52,7 @@ def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall') # rubocop:d
def acts_as_tool_call(message_class: 'Message')
@message_class = message_class.to_s

belongs_to :message, class_name: @message_class
belongs_to :message, class_name: @message_class, foreign_key: "#{@message_class.underscore}_id"

has_one :result,
class_name: @message_class,
Expand Down Expand Up @@ -170,4 +170,4 @@ def extract_content
end
end
end
end
end
10 changes: 10 additions & 0 deletions lib/ruby_llm/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@ class Railtie < Rails::Railtie
include RubyLLM::ActiveRecord::ActsAs
end
end

# Include rake tasks if applicable

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include the ruby_llm rake tasks in a Rails application:

    rake_tasks do
      path = File.expand_path(__dir__)
      Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
    end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an issue for the rake task not show up in a Rails application: #136

This is making it harder for me to use the latest Gemini models.

rake_tasks do
# Task definitions go here if needed
end

# Register generators
generators do
require 'generators/ruby_llm/install_generator'
end
end
end
Loading