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 all 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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ See the [Installation Guide](https://rubyllm.com/installation) for full details.

Add persistence to your chat models effortlessly:

### Option 1: Rails Generate

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: Manually

Or, add RubyLLM to your existing ActiveRecord models:

```ruby
# app/models/chat.rb
class Chat < ApplicationRecord
Expand Down
38 changes: 35 additions & 3 deletions docs/guides/rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,41 @@ This approach has one important consequence: **you cannot use `validates :conten

## Setting Up Your Rails Application

### Database Migrations
### Quick Setup with Generator

First, generate migrations for your `Chat`, `Message`, and `ToolCall` models.
The easiest way to get started is using the provided Rails generator:

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

This generator automatically creates:
- All required migrations (Chat, Message, ToolCall tables)
- Model files with `acts_as_chat`, `acts_as_message`, and `acts_as_tool_call` configured
- A RubyLLM initializer in `config/initializers/ruby_llm.rb`

After running the generator:

```bash
rails db:migrate
```

You're ready to go! The generator handles all the setup complexity for you.

#### Generator Options

The generator supports custom model names if needed:

```bash
# Use custom model names
rails generate ruby_llm:install --chat-model-name=Conversation --message-model-name=ChatMessage --tool-call-model-name=FunctionCall
```

This is useful if you already have models with these names or prefer different naming conventions.

### Manual Setup

If you prefer to set up manually or need custom table/model names, you can create the migrations yourself:

```bash
# Generate basic models and migrations
Expand All @@ -69,7 +101,7 @@ rails g model Message chat:references role:string content:text model_id:string i
rails g model ToolCall message:references tool_call_id:string:index name:string arguments:jsonb
```

Adjust the migrations as needed (e.g., `null: false` constraints, `jsonb` type for PostgreSQL).
Then adjust the migrations as needed (e.g., `null: false` constraints, `jsonb` type for PostgreSQL).

```ruby
# db/migrate/YYYYMMDDHHMMSS_create_chats.rb
Expand Down
75 changes: 75 additions & 0 deletions lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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
# In a background job
class ChatJob < ApplicationJob
def perform(chat_id, question)
chat = <%= options[:chat_model_name] %>.find(chat_id)
chat.ask(question) do |chunk|
Turbo::StreamsChannel.broadcast_append_to(
chat, target: "response", partial: "messages/chunk", locals: { chunk: chunk }
)
end
end
end

# Queue the job
chat = <%= options[:chat_model_name] %>.create!(model_id: "gpt-4o-mini")
ChatJob.perform_later(chat.id, "What's your favorite Ruby gem?")
```

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,15 @@
# Migration for creating messages table with references to chats and tool_calls
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
104 changes: 104 additions & 0 deletions lib/generators/ruby_llm/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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 = []
acts_as_chat_params << "message_class: \"#{options[:message_model_name]}\"" if options[:message_model_name]
acts_as_chat_params << "tool_call_class: \"#{options[:tool_call_model_name]}\"" if options[:tool_call_model_name]
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 = []
acts_as_message_params << "chat_class: \"#{options[:chat_model_name]}\"" if options[:chat_model_name]
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 = []
acts_as_tool_call_params << "message_class: \"#{options[:message_model_name]}\"" if options[:message_model_name]
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
# Create migrations with timestamps to ensure proper order
# First create chats table
timestamp = Time.now.utc
migration_template 'create_chats_migration.rb.tt',
"db/migrate/#{timestamp.strftime('%Y%m%d%H%M%S')}_create_#{options[:chat_model_name].tableize}.rb"

# Then create tool_calls table with timestamp + 1 second
timestamp += 1
migration_template 'create_tool_calls_migration.rb.tt',
"db/migrate/#{timestamp.strftime('%Y%m%d%H%M%S')}_create_#{options[:tool_call_model_name].tableize}.rb"

# Finally create messages table with timestamp + 2 seconds
timestamp += 1
migration_template 'create_messages_migration.rb.tt',
"db/migrate/#{timestamp.strftime('%Y%m%d%H%M%S')}_create_#{options[:message_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_install_info
content = ERB.new(File.read(source_paths.first + '/INSTALL_INFO.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 @@ -23,6 +23,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
11 changes: 11 additions & 0 deletions lib/ruby_llm/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,16 @@ class Railtie < Rails::Railtie
include RubyLLM::ActiveRecord::ActsAs
end
end

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

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