ruby_llm 1.6.4 → 1.7.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -3
  3. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -0
  4. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  19. data/lib/generators/ruby_llm/generator_helpers.rb +129 -0
  20. data/lib/generators/ruby_llm/install/install_generator.rb +104 -0
  21. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
  22. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
  23. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
  24. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +40 -0
  25. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
  26. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
  27. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
  28. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  29. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
  30. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  31. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +121 -0
  32. data/lib/ruby_llm/active_record/acts_as.rb +111 -327
  33. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  34. data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
  35. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  36. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  37. data/lib/ruby_llm/aliases.json +54 -13
  38. data/lib/ruby_llm/attachment.rb +20 -0
  39. data/lib/ruby_llm/chat.rb +5 -5
  40. data/lib/ruby_llm/configuration.rb +9 -0
  41. data/lib/ruby_llm/connection.rb +4 -4
  42. data/lib/ruby_llm/model/info.rb +12 -0
  43. data/lib/ruby_llm/models.json +3579 -2029
  44. data/lib/ruby_llm/models.rb +51 -22
  45. data/lib/ruby_llm/provider.rb +3 -3
  46. data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
  47. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  48. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -2
  49. data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
  50. data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
  51. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  52. data/lib/ruby_llm/providers/gpustack/chat.rb +11 -0
  53. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  54. data/lib/ruby_llm/providers/gpustack/models.rb +44 -8
  55. data/lib/ruby_llm/providers/gpustack.rb +1 -0
  56. data/lib/ruby_llm/providers/ollama/media.rb +2 -6
  57. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  58. data/lib/ruby_llm/providers/ollama.rb +1 -0
  59. data/lib/ruby_llm/providers/openai/chat.rb +1 -1
  60. data/lib/ruby_llm/providers/openai/media.rb +4 -4
  61. data/lib/ruby_llm/providers/openai/tools.rb +11 -6
  62. data/lib/ruby_llm/providers/openai.rb +2 -2
  63. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  64. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  65. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  66. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  67. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  68. data/lib/ruby_llm/railtie.rb +20 -3
  69. data/lib/ruby_llm/streaming.rb +1 -1
  70. data/lib/ruby_llm/utils.rb +5 -9
  71. data/lib/ruby_llm/version.rb +1 -1
  72. data/lib/ruby_llm.rb +4 -3
  73. data/lib/tasks/models.rake +39 -28
  74. data/lib/tasks/ruby_llm.rake +15 -0
  75. data/lib/tasks/vcr.rake +2 -2
  76. metadata +38 -3
  77. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
  78. data/lib/generators/ruby_llm/install_generator.rb +0 -121
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36ae61f8e11926aa9a761d4e8ab644eab587edb9091322596ab1ed7e32d65ee4
4
- data.tar.gz: b5818edd28a449035ed62f58a79465b8705460be012194525930716d5457d458
3
+ metadata.gz: ceecb8e7cd289f58ac1f76648306f61b54482a9b6f845a2f08b3303ee746a634
4
+ data.tar.gz: 3c075c03ccd1d7b841b99b58a1a4cda9289370b8dea039070cd78ede2d18a41c
5
5
  SHA512:
6
- metadata.gz: 35fbc3892899c17e12e239d3688542bdc032aee12d6f30d81fff8373f116eab53722cc4871654ebfe65cdd97806ec81f9903919e4a36076b2a18938d68942be0
7
- data.tar.gz: cf38979b2cf7ea03fc7d4944130fc3023d4536635d28a41f41956ea6445fc21064554fd41e917e8a3694ee798bb7a90dd10f8761c4ba326e79afd38db589e473
6
+ metadata.gz: 16ac70cc9787c0d845b5ad4c62171cb225752f160506864b07fdb0b2c1b27adc147e9799a1f13e9d7a5169595d247e5b6bc0c1083dc523647aa13e750b1d6f9d
7
+ data.tar.gz: 80543faec7119044c345f6751059e05e4cf76457ad3b288da38ea7c4e1338570fcbca1c8ad8e9dad84fb7ac5983998fd1ceba481b2e244d3542f59481a4d0e7f
data/README.md CHANGED
@@ -9,14 +9,17 @@
9
9
 
10
10
  Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
11
11
 
12
- [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=6)](https://badge.fury.io/rb/ruby_llm)
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=8)](https://badge.fury.io/rb/ruby_llm)
13
13
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
14
14
  [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm)](https://rubygems.org/gems/ruby_llm)
15
- [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=1)](https://codecov.io/gh/crmne/ruby_llm)
15
+ [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
16
16
 
17
17
  <a href="https://trendshift.io/repositories/13640" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13640" alt="crmne%2Fruby_llm | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
18
18
  </div>
19
19
 
20
+ > [!NOTE]
21
+ > Using RubyLLM in production? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
22
+
20
23
  ---
21
24
 
22
25
  Build chatbots, AI agents, RAG applications. Works with OpenAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
@@ -108,7 +111,7 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
108
111
  * **Rails:** ActiveRecord integration with `acts_as_chat`
109
112
  * **Async:** Fiber-based concurrency
110
113
  * **Model registry:** 500+ models with capability detection and pricing
111
- * **Providers:** OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
114
+ * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
112
115
 
113
116
  ## Installation
114
117
 
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generates a simple chat UI scaffold for RubyLLM
8
+ class ChatUIGenerator < Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:chat_ui'
12
+
13
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
14
+
15
+ desc 'Creates a chat UI scaffold with Turbo streaming\n' \
16
+ 'Usage: rails g ruby_llm:chat_ui [chat:ChatName] [message:MessageName] ...'
17
+
18
+ def parse_model_mappings
19
+ @model_names = {
20
+ chat: 'Chat',
21
+ message: 'Message',
22
+ tool_call: 'ToolCall',
23
+ model: 'Model'
24
+ }
25
+
26
+ model_mappings.each do |mapping|
27
+ if mapping.include?(':')
28
+ key, value = mapping.split(':', 2)
29
+ @model_names[key.to_sym] = value.classify
30
+ end
31
+ end
32
+
33
+ @model_names
34
+ end
35
+
36
+ %i[chat message model].each do |type|
37
+ define_method("#{type}_model_name") do
38
+ @model_names ||= parse_model_mappings
39
+ @model_names[type]
40
+ end
41
+
42
+ define_method("#{type}_table_name") do
43
+ table_name_for(send("#{type}_model_name"))
44
+ end
45
+ end
46
+
47
+ def create_views
48
+ # Chat views
49
+ template 'views/chats/index.html.erb', "app/views/#{chat_table_name}/index.html.erb"
50
+ template 'views/chats/new.html.erb', "app/views/#{chat_table_name}/new.html.erb"
51
+ template 'views/chats/show.html.erb', "app/views/#{chat_table_name}/show.html.erb"
52
+ template 'views/chats/_chat.html.erb',
53
+ "app/views/#{chat_table_name}/_#{chat_model_name.underscore}.html.erb"
54
+ template 'views/chats/_form.html.erb', "app/views/#{chat_table_name}/_form.html.erb"
55
+
56
+ # Message views
57
+ template 'views/messages/_message.html.erb',
58
+ "app/views/#{message_table_name}/_#{message_model_name.underscore}.html.erb"
59
+ template 'views/messages/_form.html.erb', "app/views/#{message_table_name}/_form.html.erb"
60
+ template 'views/messages/create.turbo_stream.erb',
61
+ "app/views/#{message_table_name}/create.turbo_stream.erb"
62
+
63
+ # Model views
64
+ template 'views/models/index.html.erb', "app/views/#{model_table_name}/index.html.erb"
65
+ template 'views/models/show.html.erb', "app/views/#{model_table_name}/show.html.erb"
66
+ template 'views/models/_model.html.erb',
67
+ "app/views/#{model_table_name}/_#{model_model_name.underscore}.html.erb"
68
+ end
69
+
70
+ def create_controllers
71
+ template 'controllers/chats_controller.rb', "app/controllers/#{chat_table_name}_controller.rb"
72
+ template 'controllers/messages_controller.rb', "app/controllers/#{message_table_name}_controller.rb"
73
+ template 'controllers/models_controller.rb', "app/controllers/#{model_table_name}_controller.rb"
74
+ end
75
+
76
+ def create_jobs
77
+ template 'jobs/chat_response_job.rb', "app/jobs/#{chat_model_name.underscore}_response_job.rb"
78
+ end
79
+
80
+ def add_routes
81
+ model_routes = <<~ROUTES.strip
82
+ resources :#{model_table_name}, only: [:index, :show] do
83
+ collection do
84
+ post :refresh
85
+ end
86
+ end
87
+ ROUTES
88
+ route model_routes
89
+ chat_routes = <<~ROUTES.strip
90
+ resources :#{chat_table_name} do
91
+ resources :#{message_table_name}, only: [:create]
92
+ end
93
+ ROUTES
94
+ route chat_routes
95
+ end
96
+
97
+ def add_broadcasting_to_message_model
98
+ msg_var = message_model_name.underscore
99
+ chat_var = chat_model_name.underscore
100
+ broadcasting_code = "broadcasts_to ->(#{msg_var}) { \"#{chat_var}_\#{#{msg_var}.#{chat_var}_id}\" }"
101
+
102
+ inject_into_class "app/models/#{msg_var}.rb", message_model_name do
103
+ "\n #{broadcasting_code}\n"
104
+ end
105
+ rescue Errno::ENOENT
106
+ say "#{message_model_name} model not found. Add broadcasting code to your model.", :yellow
107
+ say " #{broadcasting_code}", :yellow
108
+ end
109
+
110
+ def display_post_install_message
111
+ return unless behavior == :invoke
112
+
113
+ say "\n ✅ Chat UI installed!", :green
114
+ say "\n Start your server and visit http://localhost:3000/#{chat_table_name}", :cyan
115
+ say "\n"
116
+ end
117
+
118
+ private
119
+
120
+ def table_name_for(model_name)
121
+ # Convert namespaced model names to proper table names
122
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
123
+ model_name.underscore.pluralize.tr('/', '_')
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,39 @@
1
+ class <%= chat_model_name.pluralize %>Controller < ApplicationController
2
+ before_action :set_<%= chat_model_name.underscore %>, only: [:show]
3
+
4
+ def index
5
+ @<%= chat_model_name.tableize %> = <%= chat_model_name %>.order(created_at: :desc)
6
+ end
7
+
8
+ def new
9
+ @<%= chat_model_name.underscore %> = <%= chat_model_name %>.new
10
+ @selected_model = params[:model]
11
+ end
12
+
13
+ def create
14
+ return unless prompt.present?
15
+
16
+ @<%= chat_model_name.underscore %> = <%= chat_model_name %>.create!(model: model)
17
+ <%= chat_model_name %>ResponseJob.perform_later(@<%= chat_model_name.underscore %>.id, prompt)
18
+
19
+ redirect_to @<%= chat_model_name.underscore %>, notice: '<%= chat_model_name.humanize %> was successfully created.'
20
+ end
21
+
22
+ def show
23
+ @<%= message_model_name.underscore %> = @<%= chat_model_name.underscore %>.<%= message_model_name.tableize %>.build
24
+ end
25
+
26
+ private
27
+
28
+ def set_<%= chat_model_name.underscore %>
29
+ @<%= chat_model_name.underscore %> = <%= chat_model_name %>.find(params[:id])
30
+ end
31
+
32
+ def model
33
+ params[:<%= chat_model_name.underscore %>][:model].presence
34
+ end
35
+
36
+ def prompt
37
+ params[:<%= chat_model_name.underscore %>][:prompt]
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ class <%= message_model_name.pluralize %>Controller < ApplicationController
2
+ before_action :set_<%= chat_model_name.underscore %>
3
+
4
+ def create
5
+ return unless content.present?
6
+
7
+ <%= chat_model_name %>ResponseJob.perform_later(@<%= chat_model_name.underscore %>.id, content)
8
+
9
+ respond_to do |format|
10
+ format.turbo_stream
11
+ format.html { redirect_to @<%= chat_model_name.underscore %> }
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def set_<%= chat_model_name.underscore %>
18
+ @<%= chat_model_name.underscore %> = <%= chat_model_name %>.find(params[:<%= chat_model_name.underscore %>_id])
19
+ end
20
+
21
+ def content
22
+ params[:<%= message_model_name.underscore %>][:content]
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ class <%= model_model_name.pluralize %>Controller < ApplicationController
2
+ def index
3
+ @<%= model_model_name.tableize %> = <%= model_model_name %>.all.group_by(&:provider)
4
+ end
5
+
6
+ def show
7
+ @<%= model_model_name.underscore %> = <%= model_model_name %>.find(params[:id])
8
+ end
9
+
10
+ def refresh
11
+ <%= model_model_name %>.refresh!
12
+ redirect_to <%= model_model_name.tableize %>_path, notice: "<%= model_model_name.pluralize %> refreshed successfully"
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ class <%= chat_model_name %>ResponseJob < ApplicationJob
2
+ def perform(<%= chat_model_name.underscore %>_id, content)
3
+ <%= chat_model_name.underscore %> = <%= chat_model_name %>.find(<%= chat_model_name.underscore %>_id)
4
+
5
+ <%= chat_model_name.underscore %>.ask(content) do |chunk|
6
+ if chunk.content && !chunk.content.blank?
7
+ <%= message_model_name.underscore %> = <%= chat_model_name.underscore %>.<%= message_model_name.tableize %>.last
8
+ <%= message_model_name.underscore %>.update!(content: <%= message_model_name.underscore %>.content + chunk.content)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ <div id="<%%= dom_id <%= chat_model_name.underscore %> %>">
2
+ <div>
3
+ <strong>Model:</strong>
4
+ <%%= <%= chat_model_name.underscore %>.model_association.name %>
5
+ </div>
6
+
7
+ <div>
8
+ <strong>Messages:</strong>
9
+ <%%= <%= chat_model_name.underscore %>.messages_association.count %>
10
+ </div>
11
+
12
+ <div>
13
+ <strong>Created:</strong>
14
+ <%%= <%= chat_model_name.underscore %>.created_at.strftime("%B %d, %Y at %I:%M %p") %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,29 @@
1
+ <%%= form_with(model: <%= chat_model_name.underscore %>, url: <%= chat_model_name.tableize %>_path) do |form| %>
2
+ <%% if <%= chat_model_name.underscore %>.errors.any? %>
3
+ <div style="color: red">
4
+ <h2><%%= pluralize(<%= chat_model_name.underscore %>.errors.count, "error") %> prohibited this <%= chat_model_name.underscore.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul>
7
+ <%% <%= chat_model_name.underscore %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div>
15
+ <%%= form.label :model, "Select AI model:", style: "display: block" %>
16
+ <%%= form.select :model,
17
+ options_for_select(<%= model_model_name %>.pluck(:name, :model_id).unshift(["Default (#{RubyLLM.config.default_model})", nil]), @selected_model),
18
+ {},
19
+ style: "width: 100%; max-width: 600px; padding: 5px;" %>
20
+ </div>
21
+
22
+ <div style="margin-top: 15px;">
23
+ <%%= form.text_field :prompt, style: "width: 100%; max-width: 600px;", placeholder: "What would you like to discuss?", autofocus: true %>
24
+ </div>
25
+
26
+ <div>
27
+ <%%= form.submit "Start new <%= chat_model_name.underscore.humanize.downcase %>" %>
28
+ </div>
29
+ <%% end %>
@@ -0,0 +1,16 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <%% content_for :title, "<%= chat_model_name.pluralize %>" %>
4
+
5
+ <h1><%= chat_model_name.pluralize %></h1>
6
+
7
+ <div id="<%= chat_model_name.tableize %>">
8
+ <%% @<%= chat_model_name.tableize %>.each do |<%= chat_model_name.underscore %>| %>
9
+ <%%= render <%= chat_model_name.underscore %> %>
10
+ <p>
11
+ <%%= link_to "Show this <%= chat_model_name.underscore.humanize.downcase %>", <%= chat_model_name.underscore %> %>
12
+ </p>
13
+ <%% end %>
14
+ </div>
15
+
16
+ <%%= link_to "New <%= chat_model_name.underscore.humanize.downcase %>", new_<%= chat_model_name.underscore %>_path %>
@@ -0,0 +1,11 @@
1
+ <%% content_for :title, "New <%= chat_model_name.underscore.humanize.downcase %>" %>
2
+
3
+ <h1>New <%= chat_model_name.underscore.humanize.downcase %></h1>
4
+
5
+ <%%= render "form", <%= chat_model_name.underscore %>: @<%= chat_model_name.underscore %> %>
6
+
7
+ <br>
8
+
9
+ <div>
10
+ <%%= link_to "Back to <%= chat_model_name.tableize.humanize.downcase %>", <%= chat_model_name.tableize %>_path %>
11
+ </div>
@@ -0,0 +1,23 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <%%= turbo_stream_from "<%= chat_model_name.underscore %>_#{@<%= chat_model_name.underscore %>.id}" %>
4
+
5
+ <%% content_for :title, "<%= chat_model_name %>" %>
6
+
7
+ <h1><%= chat_model_name %> <%%= @<%= chat_model_name.underscore %>.id %></h1>
8
+
9
+ <p>Using <strong><%%= @<%= chat_model_name.underscore %>.model_association.name %></strong></p>
10
+
11
+ <div id="<%= message_model_name.tableize %>">
12
+ <%% @<%= chat_model_name.underscore %>.messages_association.where.not(id: nil).each do |<%= message_model_name.underscore %>| %>
13
+ <%%= render "<%= message_model_name.tableize %>/<%= message_model_name.underscore %>", <%= message_model_name.underscore %>: <%= message_model_name.underscore %> %>
14
+ <%% end %>
15
+ </div>
16
+
17
+ <div style="margin-top: 30px;">
18
+ <%%= render "<%= message_model_name.tableize %>/form", <%= chat_model_name.underscore %>: @<%= chat_model_name.underscore %>, <%= message_model_name.underscore %>: @<%= message_model_name.underscore %> %>
19
+ </div>
20
+
21
+ <div style="margin-top: 20px;">
22
+ <%%= link_to "Back to <%= chat_model_name.tableize.humanize.downcase %>", <%= chat_model_name.tableize %>_path %>
23
+ </div>
@@ -0,0 +1,21 @@
1
+ <%%= form_with(model: [<%= chat_model_name.underscore %>, <%= message_model_name.underscore %>], id: "new_<%= message_model_name.underscore %>") do |form| %>
2
+ <%% if <%= message_model_name.underscore %>.errors.any? %>
3
+ <div style="color: red">
4
+ <h2><%%= pluralize(<%= message_model_name.underscore %>.errors.count, "error") %> prohibited this <%= message_model_name.underscore.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul>
7
+ <%% <%= message_model_name.underscore %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div>
15
+ <%%= form.text_field :content, style: "width: 100%; max-width: 600px;", placeholder: "Message...", autofocus: true %>
16
+ </div>
17
+
18
+ <div>
19
+ <%%= form.submit "Send <%= message_model_name.underscore.humanize.downcase %>" %>
20
+ </div>
21
+ <%% end %>
@@ -0,0 +1,10 @@
1
+ <%%= tag.div id: dom_id(<%= message_model_name.underscore %>), class: "<%= message_model_name.underscore %>",
2
+ style: "margin-bottom: 20px; padding: 10px; border-left: 3px solid #{<%= message_model_name.underscore %>.role == 'user' ? '#007bff' : '#28a745'};" do %>
3
+ <div style="font-weight: bold; margin-bottom: 5px;">
4
+ <%%= <%= message_model_name.underscore %>.role&.capitalize %>
5
+ </div>
6
+ <div style="white-space: pre-wrap;"><%%= <%= message_model_name.underscore %>.content %></div>
7
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
8
+ <%%= <%= message_model_name.underscore %>.created_at&.strftime("%I:%M %p") %>
9
+ </div>
10
+ <%% end %>
@@ -0,0 +1,9 @@
1
+ <%%= turbo_stream.append "<%= message_model_name.tableize %>" do %>
2
+ <%% @<%= chat_model_name.underscore %>.messages_association.last(2).each do |<%= message_model_name.underscore %>| %>
3
+ <%%= render "<%= message_model_name.tableize %>/<%= message_model_name.underscore %>", <%= message_model_name.underscore %>: <%= message_model_name.underscore %> %>
4
+ <%% end %>
5
+ <%% end %>
6
+
7
+ <%%= turbo_stream.replace "new_<%= message_model_name.underscore %>" do %>
8
+ <%%= render "<%= message_model_name.tableize %>/form", <%= chat_model_name.underscore %>: @<%= chat_model_name.underscore %>, <%= message_model_name.underscore %>: @<%= chat_model_name.underscore %>.messages_association.build %>
9
+ <%% end %>
@@ -0,0 +1,16 @@
1
+ <tr id="<%%= dom_id <%= model_model_name.underscore %> %>">
2
+ <td><%%= <%= model_model_name.underscore %>.provider %></td>
3
+ <td><%%= <%= model_model_name.underscore %>.name %></td>
4
+ <td><%%= number_with_delimiter(<%= model_model_name.underscore %>.context_window) if <%= model_model_name.underscore %>.context_window %></td>
5
+ <td>
6
+ <%% if <%= model_model_name.underscore %>.pricing && <%= model_model_name.underscore %>.pricing['text_tokens'] && <%= model_model_name.underscore %>.pricing['text_tokens']['standard'] %>
7
+ <%% input = <%= model_model_name.underscore %>.pricing['text_tokens']['standard']['input_per_million'] %>
8
+ <%% output = <%= model_model_name.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.underscore %> %></td>
15
+ <td><%%= link_to "Start <%= chat_model_name.underscore.humanize.downcase %>", new_<%= chat_model_name.underscore %>_path(model: <%= model_model_name.underscore %>.model_id) %></td>
16
+ </tr>
@@ -0,0 +1,30 @@
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_model_name.tableize %>_path, method: :post %>
9
+ </p>
10
+
11
+ <div id="<%= model_model_name.tableize %>">
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
+ <%% @<%= model_model_name.tableize %>.values.flatten.each do |<%= model_model_name.underscore %>| %>
24
+ <%%= render <%= model_model_name.underscore %> %>
25
+ <%% end %>
26
+ </tbody>
27
+ </table>
28
+ </div>
29
+
30
+ <%%= link_to "Back to <%= chat_model_name.tableize.humanize.downcase %>", <%= chat_model_name.tableize %>_path %>
@@ -0,0 +1,18 @@
1
+ <%% content_for :title, @model.name %>
2
+
3
+ <h1><%%= @model.name %></h1>
4
+
5
+ <p><strong>ID:</strong> <%%= @model.model_id %></p>
6
+ <p><strong>Provider:</strong> <%%= @model.provider %></p>
7
+ <p><strong>Context Window:</strong> <%%= number_with_delimiter(@model.context_window) %> tokens</p>
8
+ <p><strong>Max Output:</strong> <%%= number_with_delimiter(@model.max_output_tokens) %> tokens</p>
9
+
10
+ <%% if @model.capabilities.any? %>
11
+ <p><strong>Capabilities:</strong> <%%= @model.capabilities.join(", ") %></p>
12
+ <%% end %>
13
+
14
+ <p>
15
+ <%%= link_to "Start chat with this model", new_chat_path(model: @model.model_id) %> |
16
+ <%%= link_to "All models", models_path %> |
17
+ <%%= link_to "Back to chats", chats_path %>
18
+ </p>
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Shared helpers for RubyLLM generators
5
+ module GeneratorHelpers
6
+ def parse_model_mappings
7
+ @model_names = {
8
+ chat: 'Chat',
9
+ message: 'Message',
10
+ tool_call: 'ToolCall',
11
+ model: 'Model'
12
+ }
13
+
14
+ model_mappings.each do |mapping|
15
+ if mapping.include?(':')
16
+ key, value = mapping.split(':', 2)
17
+ @model_names[key.to_sym] = value.classify
18
+ end
19
+ end
20
+
21
+ @model_names
22
+ end
23
+
24
+ %i[chat message tool_call model].each do |type|
25
+ define_method("#{type}_model_name") do
26
+ @model_names ||= parse_model_mappings
27
+ @model_names[type]
28
+ end
29
+
30
+ define_method("#{type}_table_name") do
31
+ table_name_for(send("#{type}_model_name"))
32
+ end
33
+ end
34
+
35
+ def acts_as_chat_declaration
36
+ params = []
37
+
38
+ add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
39
+ add_association_params(params, :model, model_table_name, model_model_name)
40
+
41
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
42
+ end
43
+
44
+ def acts_as_message_declaration
45
+ params = []
46
+
47
+ add_association_params(params, :chat, chat_table_name, chat_model_name)
48
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
49
+ add_association_params(params, :model, model_table_name, model_model_name)
50
+
51
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
52
+ end
53
+
54
+ def acts_as_model_declaration
55
+ params = []
56
+
57
+ add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
58
+
59
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
60
+ end
61
+
62
+ def acts_as_tool_call_declaration
63
+ params = []
64
+
65
+ add_association_params(params, :message, message_table_name, message_model_name)
66
+
67
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
68
+ end
69
+
70
+ def create_namespace_modules
71
+ namespaces = []
72
+
73
+ [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
74
+ if model_name.include?('::')
75
+ namespace = model_name.split('::').first
76
+ namespaces << namespace unless namespaces.include?(namespace)
77
+ end
78
+ end
79
+
80
+ namespaces.each do |namespace|
81
+ module_path = "app/models/#{namespace.underscore}.rb"
82
+ next if File.exist?(Rails.root.join(module_path))
83
+
84
+ create_file module_path do
85
+ <<~RUBY
86
+ module #{namespace}
87
+ def self.table_name_prefix
88
+ "#{namespace.underscore}_"
89
+ end
90
+ end
91
+ RUBY
92
+ end
93
+ end
94
+ end
95
+
96
+ def migration_version
97
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
98
+ end
99
+
100
+ def postgresql?
101
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
102
+ rescue StandardError
103
+ false
104
+ end
105
+
106
+ def table_exists?(table_name)
107
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
108
+ rescue StandardError
109
+ false
110
+ end
111
+
112
+ private
113
+
114
+ def add_association_params(params, default_assoc, table_name, model_name, plural: false)
115
+ assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
116
+
117
+ return if assoc == default_assoc
118
+
119
+ params << "#{default_assoc}: :#{assoc}"
120
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
121
+ end
122
+
123
+ def table_name_for(model_name)
124
+ # Convert namespaced model names to proper table names
125
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
126
+ model_name.underscore.pluralize.tr('/', '_')
127
+ end
128
+ end
129
+ end