-
-
Notifications
You must be signed in to change notification settings - Fork 188
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
base: main
Are you sure you want to change the base?
Changes from 16 commits
487d4e4
3577f31
24467aa
2d50909
6e68d36
dacb148
f469f60
844614b
131e51c
f40d1b4
5eeedf9
9d8c2d6
61d66ab
a239b34
8316ce5
cf4a560
492188f
52d761a
cc3d23f
9eba7bc
7cded01
21c4ee0
fda4df1
6270f22
f5454aa
5be36c1
1f8d3b0
a350738
1ed3c2e
f16858a
26bcf50
c352d5f
b064713
5a9b3b8
224b24e
1fe4320
882f3fc
4789f80
d2196bd
bf2244a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 |
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 |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah there you go that's where we can enforce the order. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,5 +8,15 @@ class Railtie < Rails::Railtie | |
include RubyLLM::ActiveRecord::ActsAs | ||
end | ||
end | ||
|
||
# Include rake tasks if applicable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should include the ruby_llm rake tasks in a Rails application:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Uh oh!
There was an error while loading. Please reload this page.