ruby_llm 1.13.2 → 1.14.0

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +33 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
  51. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
  52. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
  53. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  54. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  55. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  56. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  57. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  59. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  60. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
  61. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  64. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  65. data/lib/ruby_llm/active_record/acts_as.rb +2 -0
  66. data/lib/ruby_llm/active_record/acts_as_legacy.rb +2 -0
  67. data/lib/ruby_llm/active_record/chat_methods.rb +1 -1
  68. data/lib/ruby_llm/active_record/message_methods.rb +28 -0
  69. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  70. data/lib/ruby_llm/active_record/tool_call_methods.rb +28 -0
  71. data/lib/ruby_llm/agent.rb +11 -0
  72. data/lib/ruby_llm/aliases.json +15 -5
  73. data/lib/ruby_llm/attachment.rb +3 -0
  74. data/lib/ruby_llm/configuration.rb +54 -73
  75. data/lib/ruby_llm/connection.rb +1 -3
  76. data/lib/ruby_llm/error.rb +5 -0
  77. data/lib/ruby_llm/model/info.rb +14 -12
  78. data/lib/ruby_llm/models.json +2693 -2160
  79. data/lib/ruby_llm/models.rb +10 -3
  80. data/lib/ruby_llm/provider.rb +5 -0
  81. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  82. data/lib/ruby_llm/providers/azure.rb +4 -0
  83. data/lib/ruby_llm/providers/bedrock.rb +4 -0
  84. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  85. data/lib/ruby_llm/providers/gemini.rb +4 -0
  86. data/lib/ruby_llm/providers/gpustack.rb +4 -0
  87. data/lib/ruby_llm/providers/mistral.rb +4 -0
  88. data/lib/ruby_llm/providers/ollama.rb +4 -0
  89. data/lib/ruby_llm/providers/openai.rb +10 -0
  90. data/lib/ruby_llm/providers/openrouter/images.rb +1 -1
  91. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  92. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  93. data/lib/ruby_llm/providers/vertexai.rb +4 -0
  94. data/lib/ruby_llm/providers/xai.rb +4 -0
  95. data/lib/ruby_llm/version.rb +1 -1
  96. data/lib/tasks/release.rake +1 -1
  97. data/lib/tasks/ruby_llm.rake +6 -5
  98. data/lib/tasks/vcr.rake +1 -1
  99. metadata +47 -10
  100. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c48130380955a0e6c768f4fe305e5ed165640586b4fe68521292684cfc9fba71
4
- data.tar.gz: 510d06d85d1932eb8a4fed8d86f5bc91e05bdd5d62e8816a79d475ec31c01ac8
3
+ metadata.gz: ac485bae964a23af2c0a7ed48fa45fdb3207ad52d1ef6cbdca9b008b4c2429ba
4
+ data.tar.gz: 7219680107ccf2af1bd7378b0e1b50b2eb328280e8e896cf8f9af7ac270c761a
5
5
  SHA512:
6
- metadata.gz: 4262b4a586ad32e4145a94e95bc96187c8476c5cbaf64f63ed475027efd45f3562517c19a86aeca6dd0a5e99b08b149f13971e379f7eef012ab4ee574e5ce027
7
- data.tar.gz: 688aa065d9c5c6df785761f83258d7118c3d200e14a264e40216680ebdf49d5893fbed461487345df5bafa29b3a8db00d82710b229e1071ff4012d6aac10cd95
6
+ metadata.gz: cfed1faf8354e9be7b39cfd44bd51596eaca75fb5d810bea7da72691ed7a7494a3ba779504acc20f8e92767be522bb3d6b77fab7db435ab91994b2db21255d2f
7
+ data.tar.gz: 39b8ed657d27655a1f179fd46915da6acf81a8d0144ef2891bacb4f156311ab1be924d07018b9a095005864f3fb750a63a793d93db6a44de22c4c9bf6fab040f
data/README.md CHANGED
@@ -98,7 +98,7 @@ chat.with_tool(Weather).ask "What's the weather in Berlin?"
98
98
  ```ruby
99
99
  # Define an agent with instructions + tools
100
100
  class WeatherAssistant < RubyLLM::Agent
101
- model "gpt-4.1-nano"
101
+ model "gpt-5-nano"
102
102
  instructions "Be concise and always use tools for weather."
103
103
  tools Weather
104
104
  end
@@ -158,12 +158,12 @@ end
158
158
 
159
159
  ```bash
160
160
  # Install Rails Integration
161
- rails generate ruby_llm:install
162
- rails db:migrate
163
- rails ruby_llm:load_models # v1.13+
161
+ bin/rails generate ruby_llm:install
162
+ bin/rails db:migrate
163
+ bin/rails ruby_llm:load_models # v1.13+
164
164
 
165
165
  # Add Chat UI (optional)
166
- rails generate ruby_llm:chat_ui
166
+ bin/rails generate ruby_llm:chat_ui
167
167
  ```
168
168
 
169
169
  ```ruby
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for RubyLLM agent classes and prompt files.
8
+ class AgentGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:agent'
12
+
13
+ desc 'Creates a RubyLLM agent class and default instructions prompt'
14
+
15
+ def create_agent_file
16
+ template 'agent.rb.tt', File.join('app/agents', class_path, "#{agent_file_name}.rb")
17
+ end
18
+
19
+ def create_prompt_file
20
+ empty_directory File.join('app/prompts', class_path, agent_file_name)
21
+ template 'instructions.txt.erb.tt',
22
+ File.join('app/prompts', class_path, agent_file_name, 'instructions.txt.erb')
23
+ end
24
+
25
+ private
26
+
27
+ def agent_class_name
28
+ class_name.end_with?('Agent') ? class_name : "#{class_name}Agent"
29
+ end
30
+
31
+ def agent_file_name
32
+ agent_class_name.demodulize.underscore
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ class <%= agent_class_name %> < RubyLLM::Agent
2
+ # Change `Chat` to your app's chat model for Rails persistence.
3
+ # Remove this line to skip persistence and use plain RubyLLM chats.
4
+ chat_model Chat
5
+ instructions
6
+ end
@@ -14,9 +14,11 @@ module RubyLLM
14
14
  namespace 'ruby_llm:chat_ui'
15
15
 
16
16
  argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
17
+ class_option :ui, type: :string, default: 'auto', enum: %w[scaffold tailwind auto],
18
+ desc: 'UI template style (scaffold, tailwind, auto)'
17
19
 
18
20
  desc 'Creates a chat UI scaffold with Turbo streaming\n' \
19
- 'Usage: rails g ruby_llm:chat_ui [chat:ChatName] [message:MessageName] ...'
21
+ 'Usage: bin/rails g ruby_llm:chat_ui [chat:ChatName] [message:MessageName] ...'
20
22
 
21
23
  def check_model_exists
22
24
  model_path = "app/models/#{message_model_name.underscore}.rb"
@@ -34,40 +36,54 @@ module RubyLLM
34
36
  Model file not found: #{model_path}
35
37
 
36
38
  Please run the install generator first:
37
- rails generate ruby_llm:install#{arg_string}
39
+ bin/rails generate ruby_llm:install#{arg_string}
38
40
 
39
41
  Or if upgrading from <= 1.6.x, run the upgrade generator:
40
- rails generate ruby_llm:upgrade_to_v1_7#{arg_string}
42
+ bin/rails generate ruby_llm:upgrade_to_v1_7#{arg_string}
41
43
  ERROR
42
44
  end
43
45
 
44
46
  def create_views
47
+ # Design contract:
48
+ # - `scaffold` should stay close to Rails scaffold ERB output.
49
+ # - `tailwind` should stay close to tailwindcss-rails scaffold output.
50
+ # - Only small chat-specific affordances should be layered on top.
45
51
  # For namespaced models, use the proper Rails convention path
46
52
  chat_view_path = chat_model_name.underscore.pluralize
47
53
  message_view_path = message_model_name.underscore.pluralize
48
54
  model_view_path = model_model_name.underscore.pluralize
49
55
 
50
56
  # Chat views
51
- template 'views/chats/index.html.erb', "app/views/#{chat_view_path}/index.html.erb"
52
- template 'views/chats/new.html.erb', "app/views/#{chat_view_path}/new.html.erb"
53
- template 'views/chats/show.html.erb', "app/views/#{chat_view_path}/show.html.erb"
54
- template 'views/chats/_chat.html.erb',
57
+ template ui_template('views/chats/index.html.erb'), "app/views/#{chat_view_path}/index.html.erb"
58
+ template ui_template('views/chats/new.html.erb'), "app/views/#{chat_view_path}/new.html.erb"
59
+ template ui_template('views/chats/show.html.erb'), "app/views/#{chat_view_path}/show.html.erb"
60
+ template ui_template('views/chats/_chat.html.erb'),
55
61
  "app/views/#{chat_view_path}/_#{chat_model_name.demodulize.underscore}.html.erb"
56
- template 'views/chats/_form.html.erb', "app/views/#{chat_view_path}/_form.html.erb"
62
+ template ui_template('views/chats/_form.html.erb'), "app/views/#{chat_view_path}/_form.html.erb"
57
63
 
58
64
  # Message views
59
- template 'views/messages/_message.html.erb',
60
- "app/views/#{message_view_path}/_#{message_model_name.demodulize.underscore}.html.erb"
61
- template 'views/messages/_tool_calls.html.erb',
65
+ template ui_template('views/messages/_assistant.html.erb'), "app/views/#{message_view_path}/_assistant.html.erb"
66
+ template ui_template('views/messages/_user.html.erb'), "app/views/#{message_view_path}/_user.html.erb"
67
+ template ui_template('views/messages/_system.html.erb'), "app/views/#{message_view_path}/_system.html.erb"
68
+ template ui_template('views/messages/_tool.html.erb'), "app/views/#{message_view_path}/_tool.html.erb"
69
+ template ui_template('views/messages/_error.html.erb'), "app/views/#{message_view_path}/_error.html.erb"
70
+ template ui_template('views/messages/_tool_calls.html.erb'),
62
71
  "app/views/#{message_view_path}/_tool_calls.html.erb"
63
- template 'views/messages/_content.html.erb', "app/views/#{message_view_path}/_content.html.erb"
64
- template 'views/messages/_form.html.erb', "app/views/#{message_view_path}/_form.html.erb"
65
- template 'views/messages/create.turbo_stream.erb', "app/views/#{message_view_path}/create.turbo_stream.erb"
72
+ empty_directory "app/views/#{message_view_path}/tool_calls"
73
+ template ui_template('views/messages/tool_calls/_default.html.erb'),
74
+ "app/views/#{message_view_path}/tool_calls/_default.html.erb"
75
+ empty_directory "app/views/#{message_view_path}/tool_results"
76
+ template ui_template('views/messages/tool_results/_default.html.erb'),
77
+ "app/views/#{message_view_path}/tool_results/_default.html.erb"
78
+ template ui_template('views/messages/create.turbo_stream.erb'),
79
+ "app/views/#{message_view_path}/create.turbo_stream.erb"
80
+ template ui_template('views/messages/_content.html.erb'), "app/views/#{message_view_path}/_content.html.erb"
81
+ template ui_template('views/messages/_form.html.erb'), "app/views/#{message_view_path}/_form.html.erb"
66
82
 
67
83
  # Model views
68
- template 'views/models/index.html.erb', "app/views/#{model_view_path}/index.html.erb"
69
- template 'views/models/show.html.erb', "app/views/#{model_view_path}/show.html.erb"
70
- template 'views/models/_model.html.erb',
84
+ template ui_template('views/models/index.html.erb'), "app/views/#{model_view_path}/index.html.erb"
85
+ template ui_template('views/models/show.html.erb'), "app/views/#{model_view_path}/show.html.erb"
86
+ template ui_template('views/models/_model.html.erb'),
71
87
  "app/views/#{model_view_path}/_#{model_model_name.demodulize.underscore}.html.erb"
72
88
  end
73
89
 
@@ -86,6 +102,27 @@ module RubyLLM
86
102
  template 'jobs/chat_response_job.rb', "app/jobs/#{variable_name_for(chat_model_name)}_response_job.rb"
87
103
  end
88
104
 
105
+ def create_helpers
106
+ template 'helpers/messages_helper.rb', "app/helpers/#{message_model_name.underscore.pluralize}_helper.rb"
107
+ end
108
+
109
+ def add_available_chat_models_to_application_controller
110
+ path = 'app/controllers/application_controller.rb'
111
+ return unless File.exist?(path)
112
+
113
+ application_controller = File.read(path)
114
+ return if application_controller.include?('def available_chat_models')
115
+
116
+ inject_into_file path, <<-RUBY, before: /^end\s*\z/
117
+ private
118
+
119
+ def available_chat_models
120
+ RubyLLM.models.chat_models.all
121
+ .sort_by { |model| [ model.provider.to_s, model.name.to_s ] }
122
+ end
123
+ RUBY
124
+ end
125
+
89
126
  def add_routes
90
127
  # For namespaced models, use Rails convention with namespace blocks
91
128
  if chat_model_name.include?('::')
@@ -96,20 +133,20 @@ module RubyLLM
96
133
 
97
134
  routes_content = <<~ROUTES.strip
98
135
  namespace :#{namespace} do
99
- resources :#{model_resource}, only: [:index, :show] do
136
+ resources :#{model_resource}, only: [ :index, :show ] do
100
137
  collection do
101
138
  post :refresh
102
139
  end
103
140
  end
104
141
  resources :#{chat_resource} do
105
- resources :#{message_resource}, only: [:create]
142
+ resources :#{message_resource}, only: [ :create ]
106
143
  end
107
144
  end
108
145
  ROUTES
109
146
  route routes_content
110
147
  else
111
148
  model_routes = <<~ROUTES.strip
112
- resources :#{model_table_name}, only: [:index, :show] do
149
+ resources :#{model_table_name}, only: [ :index, :show ] do
113
150
  collection do
114
151
  post :refresh
115
152
  end
@@ -118,7 +155,7 @@ module RubyLLM
118
155
  route model_routes
119
156
  chat_routes = <<~ROUTES.strip
120
157
  resources :#{chat_table_name} do
121
- resources :#{message_table_name}, only: [:create]
158
+ resources :#{message_table_name}, only: [ :create ]
122
159
  end
123
160
  ROUTES
124
161
  route chat_routes
@@ -134,38 +171,23 @@ module RubyLLM
134
171
  # e.g., for LLM::Message, the chat association might be :llm_chat
135
172
  chat_association = chat_table_name.singularize
136
173
 
137
- # Use Rails convention paths for partials
138
- partial_path = message_model_name.underscore.pluralize
139
-
140
- # For broadcasts, we need to explicitly set the partial path
141
- # Turbo will pass the record with the demodulized name (e.g. 'message' for Llm::Message)
142
- broadcasting_code = if message_model_name.include?('::')
143
- partial_name = "#{partial_path}/#{message_model_name.demodulize.underscore}"
144
- <<~RUBY.strip
145
- broadcasts_to ->(#{msg_var}) { "#{chat_var}_\#{#{msg_var}.#{chat_association}_id}" },
146
- partial: "#{partial_name}"
147
- RUBY
148
- else
149
- "broadcasts_to ->(#{msg_var}) { \"#{chat_var}_\#{#{msg_var}.#{chat_association}_id}\" }"
150
- end
174
+ broadcasting_callbacks = <<-RUBY
151
175
 
152
- broadcast_append_chunk_method = <<-RUBY
176
+ broadcasts_to ->(#{msg_var}) { "#{chat_var}_\#{#{msg_var}.#{chat_association}_id}" }, inserts_by: :append
153
177
 
154
178
  def broadcast_append_chunk(content)
155
179
  broadcast_append_to "#{chat_var}_\#{#{chat_association}_id}",
156
180
  target: "#{msg_var}_\#{id}_content",
157
- partial: "#{partial_path}/content",
158
- locals: { content: content }
181
+ content: ERB::Util.html_escape(content.to_s)
159
182
  end
160
183
  RUBY
161
184
 
162
185
  inject_into_file "app/models/#{msg_path}.rb", before: "end\n" do
163
- " #{broadcasting_code}\n#{broadcast_append_chunk_method}"
186
+ broadcasting_callbacks
164
187
  end
165
188
  rescue Errno::ENOENT
166
189
  say "#{message_model_name} model not found. Add broadcasting code to your model.", :yellow
167
- say " #{broadcasting_code}", :yellow
168
- say broadcast_append_chunk_method, :yellow
190
+ say broadcasting_callbacks, :yellow
169
191
  end
170
192
 
171
193
  def display_post_install_message
@@ -179,9 +201,56 @@ module RubyLLM
179
201
  end
180
202
 
181
203
  say "\n ✅ Chat UI installed!", :green
204
+ say " UI template: #{ui_variant}", :cyan
182
205
  say "\n Start your server and visit http://localhost:3000/#{url_path}", :cyan
183
206
  say "\n"
184
207
  end
208
+
209
+ private
210
+
211
+ def ui_variant
212
+ @ui_variant ||= case options[:ui]
213
+ when 'tailwind'
214
+ :tailwind
215
+ when 'auto'
216
+ tailwind_available? ? :tailwind : :scaffold
217
+ else
218
+ :scaffold
219
+ end
220
+ end
221
+
222
+ def ui_template(template_path)
223
+ return template_path unless ui_variant == :tailwind
224
+
225
+ # Keep Tailwind templates as a separate set so we can mirror Rails/Tailwind
226
+ # scaffold conventions without complicating scaffold templates.
227
+ tailwind_template = "tailwind/#{template_path}"
228
+ File.exist?(File.join(self.class.source_root, "#{tailwind_template}.tt")) ? tailwind_template : template_path
229
+ end
230
+
231
+ def message_helper_module_name
232
+ if message_model_name.include?('::')
233
+ "#{message_model_name.deconstantize}::#{message_model_name.demodulize.pluralize}Helper"
234
+ else
235
+ "#{message_model_name.pluralize}Helper"
236
+ end
237
+ end
238
+
239
+ def tailwind_available?
240
+ Rails.root.join('app/assets/tailwind/application.css').exist? ||
241
+ Rails.root.join('config/tailwind.config.js').exist? ||
242
+ gem_in_bundle?('tailwindcss-rails') ||
243
+ gem_in_bundle?('cssbundling-rails')
244
+ end
245
+
246
+ def gem_in_bundle?(gem_name)
247
+ gemfile_path = Rails.root.join('Gemfile')
248
+ lockfile_path = Rails.root.join('Gemfile.lock')
249
+
250
+ [gemfile_path, lockfile_path].any? do |path|
251
+ path.exist? && path.read.include?(gem_name)
252
+ end
253
+ end
185
254
  end
186
255
  end
187
256
  end
@@ -1,5 +1,5 @@
1
1
  class <%= chat_controller_class_name %> < ApplicationController
2
- before_action :set_<%= chat_variable_name %>, only: [:show]
2
+ before_action :set_<%= chat_variable_name %>, only: [ :show, :destroy ]
3
3
 
4
4
  def index
5
5
  @<%= chat_table_name %> = <%= chat_model_name %>.order(created_at: :desc)
@@ -8,32 +8,31 @@ class <%= chat_controller_class_name %> < ApplicationController
8
8
  def new
9
9
  @<%= chat_variable_name %> = <%= chat_model_name %>.new
10
10
  @selected_model = params[:model]
11
+ @chat_models = available_chat_models
11
12
  end
12
13
 
13
14
  def create
14
- return unless prompt.present?
15
+ prompt = params.dig(:<%= chat_variable_name %>, :prompt)
16
+ if prompt.present?
17
+ @<%= chat_variable_name %> = <%= chat_model_name %>.create!(model: params.dig(:<%= chat_variable_name %>, :model).presence)
18
+ <%= chat_job_class_name %>.perform_later(@<%= chat_variable_name %>.id, prompt)
15
19
 
16
- @<%= chat_variable_name %> = <%= chat_model_name %>.create!(model: model)
17
- <%= chat_job_class_name %>.perform_later(@<%= chat_variable_name %>.id, prompt)
18
-
19
- redirect_to @<%= chat_variable_name %>, notice: '<%= chat_model_name.humanize %> was successfully created.'
20
+ redirect_to @<%= chat_variable_name %>, notice: "<%= chat_model_name.humanize %> was successfully created."
21
+ end
20
22
  end
21
23
 
22
24
  def show
23
25
  @<%= message_variable_name %> = @<%= chat_variable_name %>.<%= message_table_name %>.build
24
26
  end
25
27
 
28
+ def destroy
29
+ @<%= chat_variable_name %>.destroy!
30
+ redirect_to <%= chat_table_name %>_path, notice: "<%= chat_model_name.humanize %> was successfully destroyed.", status: :see_other
31
+ end
32
+
26
33
  private
27
34
 
28
35
  def set_<%= chat_variable_name %>
29
36
  @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:id])
30
37
  end
31
-
32
- def model
33
- params[:<%= chat_variable_name %>][:model].presence
34
- end
35
-
36
- def prompt
37
- params[:<%= chat_variable_name %>][:prompt]
38
- end
39
- end
38
+ end
@@ -2,13 +2,14 @@ class <%= message_controller_class_name %> < ApplicationController
2
2
  before_action :set_<%= chat_variable_name %>
3
3
 
4
4
  def create
5
- return unless content.present?
5
+ content = params.dig(:<%= message_variable_name %>, :content)
6
+ if content.present?
7
+ <%= chat_job_class_name %>.perform_later(@<%= chat_variable_name %>.id, content)
6
8
 
7
- <%= chat_job_class_name %>.perform_later(@<%= chat_variable_name %>.id, content)
8
-
9
- respond_to do |format|
10
- format.turbo_stream
11
- format.html { redirect_to @<%= chat_variable_name %> }
9
+ respond_to do |format|
10
+ format.turbo_stream
11
+ format.html { redirect_to @<%= chat_variable_name %> }
12
+ end
12
13
  end
13
14
  end
14
15
 
@@ -17,8 +18,4 @@ class <%= message_controller_class_name %> < ApplicationController
17
18
  def set_<%= chat_variable_name %>
18
19
  @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:<%= chat_model_name.include?('::') ? chat_model_name.demodulize.underscore : chat_variable_name %>_id])
19
20
  end
20
-
21
- def content
22
- params[:<%= message_variable_name %>][:content]
23
- end
24
- end
21
+ end
@@ -1,6 +1,6 @@
1
1
  class <%= model_controller_class_name %> < ApplicationController
2
2
  def index
3
- @<%= model_table_name %> = <%= model_model_name %>.all
3
+ @<%= model_table_name %> = available_chat_models
4
4
  end
5
5
 
6
6
  def show
@@ -11,4 +11,4 @@ class <%= model_controller_class_name %> < ApplicationController
11
11
  <%= model_model_name %>.refresh!
12
12
  redirect_to <%= model_table_name %>_path, notice: "<%= model_model_name.pluralize %> refreshed successfully"
13
13
  end
14
- end
14
+ end
@@ -0,0 +1,25 @@
1
+ module <%= message_helper_module_name %>
2
+ def default_model_display_name
3
+ "Default: #{RubyLLM.models.find(RubyLLM.config.default_model).label}"
4
+ end
5
+
6
+ def tool_result_partial(message)
7
+ name = message.respond_to?(:parent_tool_call) ? message.parent_tool_call&.name.to_s : ""
8
+ partial_for(prefix: "<%= message_model_name.underscore.pluralize %>/tool_results", name: name)
9
+ end
10
+
11
+ def tool_call_partial(tool_call)
12
+ partial_for(prefix: "<%= message_model_name.underscore.pluralize %>/tool_calls", name: tool_call.name.to_s)
13
+ end
14
+
15
+ private
16
+
17
+ def partial_for(prefix:, name:)
18
+ normalized = name.to_s.underscore.tr("-", "_")
19
+ if normalized.present? && lookup_context.exists?(normalized, [ prefix ], true)
20
+ "#{prefix}/#{normalized}"
21
+ else
22
+ "#{prefix}/default"
23
+ end
24
+ end
25
+ end
@@ -9,4 +9,4 @@ class <%= chat_job_class_name %> < ApplicationJob
9
9
  end
10
10
  end
11
11
  end
12
- end
12
+ end
@@ -0,0 +1,16 @@
1
+ <div id="<%%= dom_id <%= chat_model_name.demodulize.underscore %> %>" class="w-full sm:w-auto my-5 space-y-3">
2
+ <div>
3
+ <strong class="block font-medium mb-1">Model:</strong>
4
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= model_table_name.singularize %>&.label || default_model_display_name %>
5
+ </div>
6
+
7
+ <div>
8
+ <strong class="block font-medium mb-1">Messages:</strong>
9
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= message_table_name %>.count %>
10
+ </div>
11
+
12
+ <div>
13
+ <strong class="block font-medium mb-1">Created:</strong>
14
+ <%%= <%= chat_model_name.demodulize.underscore %>.created_at.strftime("%B %d, %Y at %I:%M %p") %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,31 @@
1
+ <%%= form_with(model: <%= chat_variable_name %>, url: <%= chat_table_name %>_path, class: "contents") do |form| %>
2
+ <%% if <%= chat_variable_name %>.errors.any? %>
3
+ <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3">
4
+ <h2><%%= pluralize(<%= chat_variable_name %>.errors.count, "error") %> prohibited this <%= chat_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul class="list-disc ml-6">
7
+ <%% <%= chat_variable_name %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div class="my-5">
15
+ <%%= form.label :model, "Select AI model:" %>
16
+ <%%= form.select :model,
17
+ options_for_select(@chat_models.map { |model| [model.label, model.id] }.unshift([default_model_display_name, nil]), @selected_model),
18
+ {},
19
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= chat_variable_name %>.errors[:model].none?, "border-red-400 focus:outline-red-600": <%= chat_variable_name %>.errors[:model].any?}] %>
20
+ </div>
21
+
22
+ <div class="my-5">
23
+ <%%= form.label :prompt, "Prompt" %>
24
+ <%%= form.text_area :prompt, rows: 4, placeholder: "What would you like to discuss?", autofocus: true,
25
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= chat_variable_name %>.errors[:prompt].none?, "border-red-400 focus:outline-red-600": <%= chat_variable_name %>.errors[:prompt].any?}] %>
26
+ </div>
27
+
28
+ <div class="inline">
29
+ <%%= form.submit "Start new <%= chat_table_name.singularize.humanize.downcase %>", class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
30
+ </div>
31
+ <%% end %>
@@ -0,0 +1,31 @@
1
+ <%% content_for :title, "<%= chat_model_name.pluralize %>" %>
2
+
3
+ <div class="w-full">
4
+ <%% if notice.present? %>
5
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
6
+ <%% end %>
7
+
8
+ <div class="flex justify-between items-center">
9
+ <h1 class="font-bold text-4xl"><%= chat_model_name.pluralize %></h1>
10
+ <div class="flex items-center gap-2">
11
+ <%%= link_to "<%= model_model_name.pluralize %>", <%= model_table_name %>_path, class: "rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 text-gray-900 block font-medium" %>
12
+ <%%= link_to "New <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div id="<%= chat_table_name %>" class="min-w-full divide-y divide-gray-200 space-y-5">
17
+ <%% if @<%= chat_table_name %>.any? %>
18
+ <%% @<%= chat_table_name %>.each do |<%= chat_variable_name %>| %>
19
+ <div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
20
+ <%%= render <%= chat_variable_name %> %>
21
+ <div class="w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2">
22
+ <%%= link_to "Show", <%= chat_variable_name %>, class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
23
+ <%%= button_to "Destroy", <%= chat_variable_name %>, method: :delete, class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-red-600 hover:bg-red-500 text-white inline-block font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
24
+ </div>
25
+ </div>
26
+ <%% end %>
27
+ <%% else %>
28
+ <p class="text-center my-10">No <%= chat_table_name.humanize.downcase %> found.</p>
29
+ <%% end %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,9 @@
1
+ <%% content_for :title, "New <%= chat_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <div class="md:w-2/3 w-full">
4
+ <h1 class="font-bold text-4xl">New <%= chat_table_name.singularize.humanize.downcase %></h1>
5
+
6
+ <%%= render "form", <%= chat_variable_name %>: @<%= chat_variable_name %> %>
7
+
8
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
9
+ </div>
@@ -0,0 +1,27 @@
1
+ <%% content_for :title, "Showing <%= chat_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <%%= turbo_stream_from "<%= chat_variable_name %>_#{@<%= chat_variable_name %>.id}" %>
4
+
5
+ <%# Keep layout conventions aligned with tailwindcss-rails scaffold (top-left, md:w-2/3). -%>
6
+ <div class="md:w-2/3 w-full">
7
+ <%% if notice.present? %>
8
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
9
+ <%% end %>
10
+
11
+ <h1 class="font-bold text-4xl">Showing <%= chat_table_name.singularize.humanize.downcase %> #<%%= @<%= chat_variable_name %>.id %></h1>
12
+
13
+ <div class="my-5">
14
+ <strong class="block font-medium mb-1">Model:</strong>
15
+ <%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>&.label || default_model_display_name %>
16
+ </div>
17
+
18
+ <div id="<%= message_table_name %>" class="min-w-full divide-y divide-gray-200 space-y-5 my-5">
19
+ <%% @<%= chat_variable_name %>.<%= message_table_name %>.where.not(id: nil).each do |<%= message_variable_name %>| %>
20
+ <%%= render <%= message_variable_name %> %>
21
+ <%% end %>
22
+ </div>
23
+
24
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= message_variable_name %> %>
25
+
26
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
27
+ </div>
@@ -0,0 +1,14 @@
1
+ <%% assistant ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-green-50">
3
+ <div>
4
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700">
5
+ Assistant
6
+ </span>
7
+ </div>
8
+
9
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>_content" class="whitespace-pre-wrap"><%%= assistant.content %></div>
10
+
11
+ <div>
12
+ <span class="text-sm text-gray-600"><%%= assistant.created_at&.strftime("%I:%M %p") %></span>
13
+ </div>
14
+ </div>
@@ -0,0 +1,13 @@
1
+ <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-red-50 border border-red-200">
2
+ <div>
3
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700">
4
+ <%%= title.presence || "Error" %>
5
+ </span>
6
+ </div>
7
+
8
+ <pre class="whitespace-pre-wrap text-red-900 text-sm overflow-x-auto"><%%= error_message %></pre>
9
+
10
+ <div>
11
+ <span class="text-sm text-red-700"><%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %></span>
12
+ </div>
13
+ </div>
@@ -0,0 +1,23 @@
1
+ <%%= form_with(model: <%= message_variable_name %>, url: <%= chat_model_name.include?('::') ? "#{chat_model_name.split('::').first.underscore}_#{chat_model_name.demodulize.underscore}_#{message_model_name.demodulize.underscore.pluralize}_path(@#{chat_variable_name})" : "[@#{chat_variable_name}, #{message_variable_name}]" %>, id: "new_<%= message_variable_name %>", class: "contents") do |form| %>
2
+ <%% if <%= message_variable_name %>.errors.any? %>
3
+ <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3">
4
+ <h2><%%= pluralize(<%= message_variable_name %>.errors.count, "error") %> prohibited this <%= message_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul class="list-disc ml-6">
7
+ <%% <%= message_variable_name %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div class="my-5">
15
+ <%%= form.label :content, "Message" %>
16
+ <%%= form.text_area :content, rows: 4, placeholder: "Message...", autofocus: true,
17
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= message_variable_name %>.errors[:content].none?, "border-red-400 focus:outline-red-600": <%= message_variable_name %>.errors[:content].any?}] %>
18
+ </div>
19
+
20
+ <div class="inline">
21
+ <%%= form.submit "Send <%= message_table_name.singularize.humanize.downcase %>", class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
22
+ </div>
23
+ <%% end %>