ruby_llm_swarm 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
- data/lib/ruby_llm/active_record/acts_as.rb +174 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
- data/lib/ruby_llm/active_record/message_methods.rb +81 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +295 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +220 -0
- data/lib/ruby_llm/chat.rb +816 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +78 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +73 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +86 -0
- data/lib/ruby_llm/mime_type.rb +71 -0
- data/lib/ruby_llm/model/info.rb +111 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +33198 -0
- data/lib/ruby_llm/models.rb +231 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/moderation.rb +56 -0
- data/lib/ruby_llm/provider.rb +243 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
- data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +112 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
- data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
- data/lib/ruby_llm/providers/gemini.rb +37 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +46 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
- data/lib/ruby_llm/providers/openai/chat.rb +88 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
- data/lib/ruby_llm/providers/openai/tools.rb +98 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_responses.rb +395 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +35 -0
- data/lib/ruby_llm/responses_session.rb +77 -0
- data/lib/ruby_llm/stream_accumulator.rb +101 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +209 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/tool_executors.rb +125 -0
- data/lib/ruby_llm/transcription.rb +35 -0
- data/lib/ruby_llm/utils.rb +91 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +140 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +346 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<tr id="<%%= dom_id <%= model_model_name.demodulize.underscore %> %>">
|
|
2
|
+
<td><%%= <%= model_model_name.demodulize.underscore %>.provider %></td>
|
|
3
|
+
<td><%%= <%= model_model_name.demodulize.underscore %>.name %></td>
|
|
4
|
+
<td><%%= number_with_delimiter(<%= model_model_name.demodulize.underscore %>.context_window) if <%= model_model_name.demodulize.underscore %>.context_window %></td>
|
|
5
|
+
<td>
|
|
6
|
+
<%% if <%= model_model_name.demodulize.underscore %>.pricing && <%= model_model_name.demodulize.underscore %>.pricing['text_tokens'] && <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard'] %>
|
|
7
|
+
<%% input = <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard']['input_per_million'] %>
|
|
8
|
+
<%% output = <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard']['output_per_million'] %>
|
|
9
|
+
<%% if input && output %>
|
|
10
|
+
$<%%= "%.2f" % input %> / $<%%= "%.2f" % output %>
|
|
11
|
+
<%% end %>
|
|
12
|
+
<%% end %>
|
|
13
|
+
</td>
|
|
14
|
+
<td><%%= link_to "Show", <%= model_model_name.demodulize.underscore %> %></td>
|
|
15
|
+
<td><%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path(model: <%= model_model_name.demodulize.underscore %>.model_id) %></td>
|
|
16
|
+
</tr>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<p style="color: green"><%%= notice %></p>
|
|
2
|
+
|
|
3
|
+
<%% content_for :title, "<%= model_model_name.pluralize %>" %>
|
|
4
|
+
|
|
5
|
+
<h1><%= model_model_name.pluralize %></h1>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post %>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<div id="<%= model_table_name %>">
|
|
12
|
+
<table>
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th>Provider</th>
|
|
16
|
+
<th>Model</th>
|
|
17
|
+
<th>Context Window</th>
|
|
18
|
+
<th>$/1M tokens (In/Out)</th>
|
|
19
|
+
<th colspan="2"></th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
<tbody>
|
|
23
|
+
<%%= render @<%= model_variable_name.pluralize %> %>
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%% content_for :title, @<%= model_variable_name %>.name %>
|
|
2
|
+
|
|
3
|
+
<h1><%%= @<%= model_variable_name %>.name %></h1>
|
|
4
|
+
|
|
5
|
+
<p><strong>ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
|
|
6
|
+
<p><strong>Provider:</strong> <%%= @<%= model_variable_name %>.provider %></p>
|
|
7
|
+
<p><strong>Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) %> tokens</p>
|
|
8
|
+
<p><strong>Max Output:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) %> tokens</p>
|
|
9
|
+
|
|
10
|
+
<%% if @<%= model_variable_name %>.capabilities.any? %>
|
|
11
|
+
<p><strong>Capabilities:</strong> <%%= @<%= model_variable_name %>.capabilities.join(", ") %></p>
|
|
12
|
+
<%% end %>
|
|
13
|
+
|
|
14
|
+
<p>
|
|
15
|
+
<%%= link_to "Start chat with this model", new_<%= chat_variable_name %>_path(model: @<%= model_variable_name %>.model_id) %> |
|
|
16
|
+
<%%= link_to "All models", <%= model_table_name %>_path %> |
|
|
17
|
+
<%%= link_to "Back to chats", <%= chat_table_name %>_path %>
|
|
18
|
+
</p>
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Generators
|
|
5
|
+
# Shared helpers for RubyLLM generators
|
|
6
|
+
module GeneratorHelpers
|
|
7
|
+
def parse_model_mappings
|
|
8
|
+
@model_names = {
|
|
9
|
+
chat: 'Chat',
|
|
10
|
+
message: 'Message',
|
|
11
|
+
tool_call: 'ToolCall',
|
|
12
|
+
model: 'Model'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
model_mappings.each do |mapping|
|
|
16
|
+
if mapping.include?(':')
|
|
17
|
+
key, value = mapping.split(':', 2)
|
|
18
|
+
@model_names[key.to_sym] = value.classify
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@model_names
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
%i[chat message tool_call model].each do |type|
|
|
26
|
+
define_method("#{type}_model_name") do
|
|
27
|
+
@model_names ||= parse_model_mappings
|
|
28
|
+
@model_names[type]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
define_method("#{type}_table_name") do
|
|
32
|
+
table_name_for(send("#{type}_model_name"))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_method("#{type}_variable_name") do
|
|
36
|
+
variable_name_for(send("#{type}_model_name"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_method("#{type}_controller_class_name") do
|
|
40
|
+
controller_class_name_for(send("#{type}_model_name"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
define_method("#{type}_job_class_name") do
|
|
44
|
+
"#{variable_name_for(send("#{type}_model_name")).camelize}ResponseJob"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
define_method("#{type}_partial") do
|
|
48
|
+
partial_path_for(send("#{type}_model_name"))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def acts_as_chat_declaration
|
|
53
|
+
params = []
|
|
54
|
+
|
|
55
|
+
add_association_params(params, :messages, message_table_name, message_model_name,
|
|
56
|
+
owner_table: chat_table_name, plural: true)
|
|
57
|
+
add_association_params(params, :model, model_table_name, model_model_name,
|
|
58
|
+
owner_table: chat_table_name)
|
|
59
|
+
|
|
60
|
+
"acts_as_chat#{" #{params.join(', ')}" if params.any?}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def acts_as_message_declaration
|
|
64
|
+
params = []
|
|
65
|
+
|
|
66
|
+
add_association_params(params, :chat, chat_table_name, chat_model_name,
|
|
67
|
+
owner_table: message_table_name)
|
|
68
|
+
add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
|
|
69
|
+
owner_table: message_table_name, plural: true)
|
|
70
|
+
add_association_params(params, :model, model_table_name, model_model_name,
|
|
71
|
+
owner_table: message_table_name)
|
|
72
|
+
|
|
73
|
+
"acts_as_message#{" #{params.join(', ')}" if params.any?}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def acts_as_model_declaration
|
|
77
|
+
params = []
|
|
78
|
+
|
|
79
|
+
add_association_params(params, :chats, chat_table_name, chat_model_name,
|
|
80
|
+
owner_table: model_table_name, plural: true)
|
|
81
|
+
|
|
82
|
+
"acts_as_model#{" #{params.join(', ')}" if params.any?}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def acts_as_tool_call_declaration
|
|
86
|
+
params = []
|
|
87
|
+
|
|
88
|
+
add_association_params(params, :message, message_table_name, message_model_name,
|
|
89
|
+
owner_table: tool_call_table_name)
|
|
90
|
+
|
|
91
|
+
"acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def create_namespace_modules
|
|
95
|
+
namespaces = []
|
|
96
|
+
|
|
97
|
+
[chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
|
|
98
|
+
if model_name.include?('::')
|
|
99
|
+
namespace = model_name.split('::').first
|
|
100
|
+
namespaces << namespace unless namespaces.include?(namespace)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
namespaces.each do |namespace|
|
|
105
|
+
module_path = "app/models/#{namespace.underscore}.rb"
|
|
106
|
+
next if File.exist?(Rails.root.join(module_path))
|
|
107
|
+
|
|
108
|
+
create_file module_path do
|
|
109
|
+
<<~RUBY
|
|
110
|
+
module #{namespace}
|
|
111
|
+
def self.table_name_prefix
|
|
112
|
+
"#{namespace.underscore}_"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
RUBY
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def migration_version
|
|
121
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def postgresql?
|
|
125
|
+
::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
|
|
126
|
+
rescue StandardError
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def mysql?
|
|
131
|
+
::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
|
|
132
|
+
rescue StandardError
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def table_exists?(table_name)
|
|
137
|
+
::ActiveRecord::Base.connection.table_exists?(table_name)
|
|
138
|
+
rescue StandardError
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def add_association_params(params, default_assoc, table_name, model_name, owner_table:, plural: false) # rubocop:disable Metrics/ParameterLists
|
|
145
|
+
assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
|
|
146
|
+
|
|
147
|
+
default_foreign_key = "#{default_assoc}_id"
|
|
148
|
+
# has_many/has_one: foreign key is on the associated table pointing back to owner
|
|
149
|
+
# belongs_to: foreign key is on the owner table pointing to associated table
|
|
150
|
+
foreign_key = if plural || default_assoc.to_s.pluralize == default_assoc.to_s # has_many or has_one
|
|
151
|
+
"#{owner_table.singularize}_id"
|
|
152
|
+
else # belongs_to
|
|
153
|
+
"#{table_name.singularize}_id"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
|
|
157
|
+
params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
|
|
158
|
+
params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Convert namespaced model names to proper table names
|
|
162
|
+
# e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
|
|
163
|
+
def table_name_for(model_name)
|
|
164
|
+
model_name.underscore.pluralize.tr('/', '_')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Convert model name to instance variable name
|
|
168
|
+
# e.g., "LLM::Chat" -> "llm_chat" (not "llm/chat")
|
|
169
|
+
def variable_name_for(model_name)
|
|
170
|
+
model_name.underscore.tr('/', '_')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Convert model name to controller class name
|
|
174
|
+
# For namespaced models, use Rails convention: "Llm::Chat" -> "Llm::ChatsController"
|
|
175
|
+
# For regular models: "Chat" -> "ChatsController"
|
|
176
|
+
def controller_class_name_for(model_name)
|
|
177
|
+
if model_name.include?('::')
|
|
178
|
+
parts = model_name.split('::')
|
|
179
|
+
namespace = parts[0..-2].join('::')
|
|
180
|
+
resource = parts.last.pluralize
|
|
181
|
+
"#{namespace}::#{resource}Controller"
|
|
182
|
+
else
|
|
183
|
+
"#{model_name.pluralize}Controller"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Convert model name to partial path
|
|
188
|
+
# e.g., "LLM::Message" -> "llm/message" (not "llm_message")
|
|
189
|
+
def partial_path_for(model_name)
|
|
190
|
+
"#{model_name.underscore.pluralize}/#{model_name.demodulize.underscore}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../generator_helpers'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Generators
|
|
9
|
+
# Generator for RubyLLM Rails models and migrations
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
include RubyLLM::Generators::GeneratorHelpers
|
|
13
|
+
|
|
14
|
+
namespace 'ruby_llm:install'
|
|
15
|
+
|
|
16
|
+
source_root File.expand_path('templates', __dir__)
|
|
17
|
+
|
|
18
|
+
argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
|
|
19
|
+
|
|
20
|
+
class_option :skip_active_storage, type: :boolean, default: false,
|
|
21
|
+
desc: 'Skip ActiveStorage installation and attachment setup'
|
|
22
|
+
|
|
23
|
+
desc 'Creates models and migrations for RubyLLM Rails integration\n' \
|
|
24
|
+
'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
|
|
25
|
+
|
|
26
|
+
def self.next_migration_number(dirname)
|
|
27
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_migration_files
|
|
31
|
+
migration_template 'create_chats_migration.rb.tt',
|
|
32
|
+
"db/migrate/create_#{chat_table_name}.rb"
|
|
33
|
+
|
|
34
|
+
sleep 1 # Ensure different timestamp
|
|
35
|
+
migration_template 'create_messages_migration.rb.tt',
|
|
36
|
+
"db/migrate/create_#{message_table_name}.rb"
|
|
37
|
+
|
|
38
|
+
sleep 1 # Ensure different timestamp
|
|
39
|
+
migration_template 'create_tool_calls_migration.rb.tt',
|
|
40
|
+
"db/migrate/create_#{tool_call_table_name}.rb"
|
|
41
|
+
|
|
42
|
+
sleep 1 # Ensure different timestamp
|
|
43
|
+
migration_template 'create_models_migration.rb.tt',
|
|
44
|
+
"db/migrate/create_#{model_table_name}.rb"
|
|
45
|
+
|
|
46
|
+
sleep 1 # Ensure different timestamp
|
|
47
|
+
migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
|
|
48
|
+
'db/migrate/add_references_to_' \
|
|
49
|
+
"#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_model_files
|
|
53
|
+
create_namespace_modules
|
|
54
|
+
|
|
55
|
+
template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
|
|
56
|
+
template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
|
|
57
|
+
template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
|
|
58
|
+
|
|
59
|
+
template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_initializer
|
|
63
|
+
template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def install_active_storage
|
|
67
|
+
return if options[:skip_active_storage]
|
|
68
|
+
|
|
69
|
+
say ' Installing ActiveStorage for file attachments...', :cyan
|
|
70
|
+
rails_command 'active_storage:install'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def show_install_info
|
|
74
|
+
say "\n ✅ RubyLLM installed!", :green
|
|
75
|
+
|
|
76
|
+
say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
|
|
77
|
+
|
|
78
|
+
say "\n Next steps:", :yellow
|
|
79
|
+
say ' 1. Run: rails db:migrate'
|
|
80
|
+
say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
|
|
81
|
+
|
|
82
|
+
say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
|
|
83
|
+
|
|
84
|
+
say "\n 🚀 Model registry is database-backed!", :cyan
|
|
85
|
+
say ' Models automatically load from the database'
|
|
86
|
+
say ' Pass model names as strings - RubyLLM handles the rest!'
|
|
87
|
+
say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
|
|
88
|
+
|
|
89
|
+
if options[:skip_active_storage]
|
|
90
|
+
say "\n 📎 Note: ActiveStorage was skipped", :yellow
|
|
91
|
+
say ' File attachments won\'t work without ActiveStorage.'
|
|
92
|
+
say ' To enable later:'
|
|
93
|
+
say ' 1. Run: rails active_storage:install && rails db:migrate'
|
|
94
|
+
say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
say "\n 📚 Documentation: https://rubyllm.com", :cyan
|
|
98
|
+
|
|
99
|
+
say "\n ❤️ Love RubyLLM?", :magenta
|
|
100
|
+
say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
|
|
101
|
+
say ' • 🐦 Follow for updates: https://x.com/paolino'
|
|
102
|
+
say "\n"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddReferencesTo<%= "#{chat_model_name.gsub('::', '').pluralize}#{tool_call_model_name.gsub('::', '').pluralize}And#{message_model_name.gsub('::', '').pluralize}" %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
add_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
4
|
+
add_reference :<%= tool_call_table_name %>, :<%= message_table_name.singularize %>, null: false, foreign_key: true
|
|
5
|
+
add_reference :<%= message_table_name %>, :<%= chat_table_name.singularize %>, null: false, foreign_key: true
|
|
6
|
+
add_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
7
|
+
add_reference :<%= message_table_name %>, :<%= tool_call_table_name.singularize %>, foreign_key: true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= message_table_name %> do |t|
|
|
4
|
+
t.string :role, null: false
|
|
5
|
+
t.text :content
|
|
6
|
+
t.json :content_raw
|
|
7
|
+
t.integer :input_tokens
|
|
8
|
+
t.integer :output_tokens
|
|
9
|
+
t.integer :cached_tokens
|
|
10
|
+
t.integer :cache_creation_tokens
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :<%= message_table_name %>, :role
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= model_table_name %> do |t|
|
|
4
|
+
t.string :model_id, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :provider, null: false
|
|
7
|
+
t.string :family
|
|
8
|
+
t.datetime :model_created_at
|
|
9
|
+
t.integer :context_window
|
|
10
|
+
t.integer :max_output_tokens
|
|
11
|
+
t.date :knowledge_cutoff
|
|
12
|
+
<% if postgresql? %>
|
|
13
|
+
t.jsonb :modalities, default: {}
|
|
14
|
+
t.jsonb :capabilities, default: []
|
|
15
|
+
t.jsonb :pricing, default: {}
|
|
16
|
+
t.jsonb :metadata, default: {}
|
|
17
|
+
<% elsif mysql? %>
|
|
18
|
+
t.json :modalities
|
|
19
|
+
t.json :capabilities
|
|
20
|
+
t.json :pricing
|
|
21
|
+
t.json :metadata
|
|
22
|
+
<% else %>
|
|
23
|
+
t.json :modalities, default: {}
|
|
24
|
+
t.json :capabilities, default: []
|
|
25
|
+
t.json :pricing, default: {}
|
|
26
|
+
t.json :metadata, default: {}
|
|
27
|
+
<% end %>
|
|
28
|
+
t.timestamps
|
|
29
|
+
|
|
30
|
+
t.index [:provider, :model_id], unique: true
|
|
31
|
+
t.index :provider
|
|
32
|
+
t.index :family
|
|
33
|
+
<% if postgresql? %>
|
|
34
|
+
t.index :capabilities, using: :gin
|
|
35
|
+
t.index :modalities, using: :gin
|
|
36
|
+
<% end %>
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Load models from JSON
|
|
40
|
+
say_with_time "Loading models from models.json" do
|
|
41
|
+
RubyLLM.models.load_from_json!
|
|
42
|
+
<%= model_model_name %>.save_to_database
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
|
|
2
|
+
class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
3
|
+
def change
|
|
4
|
+
create_table :<%= tool_call_table_name %> do |t|
|
|
5
|
+
t.string :tool_call_id, null: false
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
<% if postgresql? %>
|
|
8
|
+
t.jsonb :arguments, default: {}
|
|
9
|
+
<% elsif mysql? %>
|
|
10
|
+
t.json :arguments
|
|
11
|
+
<% else %>
|
|
12
|
+
t.json :arguments, default: {}
|
|
13
|
+
<% end %>
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :<%= tool_call_table_name %>, :tool_call_id, unique: true
|
|
18
|
+
add_index :<%= tool_call_table_name %>, :name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
RubyLLM.configure do |config|
|
|
2
|
+
config.openai_api_key = ENV['OPENAI_API_KEY'] || Rails.application.credentials.dig(:openai_api_key)
|
|
3
|
+
# config.default_model = "gpt-4.1-nano"
|
|
4
|
+
|
|
5
|
+
# Use the new association-based acts_as API (recommended)
|
|
6
|
+
config.use_new_acts_as = true
|
|
7
|
+
<% if model_model_name != 'Model' -%>
|
|
8
|
+
|
|
9
|
+
# Custom model registry class name
|
|
10
|
+
config.model_registry_class = "<%= model_model_name %>"
|
|
11
|
+
<% end -%>
|
|
12
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
model_class = <%= model_model_name %>
|
|
4
|
+
chat_class = <%= chat_model_name %>
|
|
5
|
+
message_class = <%= message_model_name %>
|
|
6
|
+
<% if @model_table_already_existed %>
|
|
7
|
+
# Load models from models.json if Model table already existed
|
|
8
|
+
say_with_time "Loading models from models.json" do
|
|
9
|
+
RubyLLM.models.load_from_json!
|
|
10
|
+
model_class.save_to_database
|
|
11
|
+
"Loaded #{model_class.count} models"
|
|
12
|
+
end
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
# Then check for any models in existing data that aren't in models.json
|
|
16
|
+
say_with_time "Checking for additional models in existing data" do
|
|
17
|
+
collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
|
|
18
|
+
collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
|
|
19
|
+
model_class.count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Migrate foreign keys
|
|
23
|
+
migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
|
|
24
|
+
migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def down
|
|
28
|
+
# Remove foreign key references
|
|
29
|
+
if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
|
|
30
|
+
remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
|
|
34
|
+
remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Restore original model_id string columns
|
|
38
|
+
if column_exists?(:<%= message_table_name %>, :model_id_string)
|
|
39
|
+
rename_column :<%= message_table_name %>, :model_id_string, :model_id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if column_exists?(:<%= chat_table_name %>, :model_id_string)
|
|
43
|
+
rename_column :<%= chat_table_name %>, :model_id_string, :model_id
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collect_and_create_models(record_class, table_name, model_class)
|
|
50
|
+
return unless column_exists?(table_name, :model_id)
|
|
51
|
+
|
|
52
|
+
has_provider = column_exists?(table_name, :provider)
|
|
53
|
+
|
|
54
|
+
# Collect unique model/provider combinations using read_attribute to bypass overrides
|
|
55
|
+
models_set = Set.new
|
|
56
|
+
|
|
57
|
+
record_class.find_each do |record|
|
|
58
|
+
model_id = record.read_attribute(:model_id)
|
|
59
|
+
next if model_id.blank?
|
|
60
|
+
|
|
61
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
|
62
|
+
models_set.add([ model_id, provider ])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
models_set.each do |model_id, provider|
|
|
66
|
+
find_or_create_model(model_id, provider, model_class)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_or_create_model(model_id, provider, model_class)
|
|
71
|
+
return if model_id.blank?
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
|
|
75
|
+
|
|
76
|
+
model_class.find_or_create_by!(
|
|
77
|
+
model_id: model_info.id,
|
|
78
|
+
provider: model_info.provider
|
|
79
|
+
) do |m|
|
|
80
|
+
m.name = model_info.name || model_info.id
|
|
81
|
+
m.family = model_info.family
|
|
82
|
+
m.model_created_at = model_info.created_at
|
|
83
|
+
m.context_window = model_info.context_window
|
|
84
|
+
m.max_output_tokens = model_info.max_output_tokens
|
|
85
|
+
m.knowledge_cutoff = model_info.knowledge_cutoff
|
|
86
|
+
m.modalities = model_info.modalities.to_h
|
|
87
|
+
m.capabilities = model_info.capabilities
|
|
88
|
+
m.pricing = model_info.pricing.to_h
|
|
89
|
+
m.metadata = model_info.metadata
|
|
90
|
+
end
|
|
91
|
+
rescue => e
|
|
92
|
+
# Skip models that can't be resolved - they'll need manual fixing
|
|
93
|
+
Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
|
|
100
|
+
return unless column_exists?(table_name, :model_id)
|
|
101
|
+
|
|
102
|
+
# Check if we need to rename the string column to avoid collision
|
|
103
|
+
if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
|
|
104
|
+
# Temporarily rename the string column
|
|
105
|
+
rename_column table_name, :model_id, :model_id_string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Add the foreign key reference
|
|
109
|
+
unless column_exists?(table_name, "#{foreign_key_name}_id")
|
|
110
|
+
add_reference table_name, foreign_key_name, foreign_key: true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
say_with_time "Migrating #{table_name} model references" do
|
|
114
|
+
record_class.reset_column_information
|
|
115
|
+
has_provider = column_exists?(table_name, :provider)
|
|
116
|
+
|
|
117
|
+
# Determine which column to read from (renamed or original)
|
|
118
|
+
model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
|
|
119
|
+
|
|
120
|
+
record_class.find_each do |record|
|
|
121
|
+
model_id = record.read_attribute(model_id_column)
|
|
122
|
+
next if model_id.blank?
|
|
123
|
+
|
|
124
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
|
125
|
+
|
|
126
|
+
model = if has_provider && provider.present?
|
|
127
|
+
model_class.find_by(model_id: model_id, provider: provider)
|
|
128
|
+
else
|
|
129
|
+
find_model_for_record(model_id, model_class)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
record.update_column("#{foreign_key_name}_id", model.id) if model
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_model_for_record(model_id, model_class)
|
|
138
|
+
begin
|
|
139
|
+
model_info, _provider = RubyLLM.models.resolve(model_id)
|
|
140
|
+
model_class.find_by(model_id: model_info.id, provider: model_info.provider)
|
|
141
|
+
rescue => e
|
|
142
|
+
model_class.find_by(model_id: model_id)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|