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
@@ -0,0 +1,10 @@
1
+ <%% system ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= system.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
3
+ <div>
4
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
5
+ System
6
+ </span>
7
+ </div>
8
+
9
+ <div class="whitespace-pre-wrap text-gray-700"><%%= system.content %></div>
10
+ </div>
@@ -0,0 +1,2 @@
1
+ <%% tool ||= local_assigns[:message] %>
2
+ <%%= render tool_result_partial(tool), tool: tool %>
@@ -0,0 +1,4 @@
1
+ <%% tool_calls ||= local_assigns[:message] %>
2
+ <%% tool_calls.<%= tool_call_variable_name.pluralize %>.each do |tool_call| %>
3
+ <%%= render tool_call_partial(tool_call), tool_calls: tool_calls, tool_call: tool_call %>
4
+ <%% end %>
@@ -0,0 +1,14 @@
1
+ <%% user ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= user.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-blue-50">
3
+ <div>
4
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-700">
5
+ User
6
+ </span>
7
+ </div>
8
+
9
+ <div id="<%= message_variable_name %>_<%%= user.id %>_content" class="whitespace-pre-wrap"><%%= user.content %></div>
10
+
11
+ <div>
12
+ <span class="text-sm text-gray-600"><%%= user.created_at&.strftime("%I:%M %p") %></span>
13
+ </div>
14
+ </div>
@@ -0,0 +1,13 @@
1
+ <div id="<%= message_variable_name %>_<%%= tool_calls.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
2
+ <div>
3
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
4
+ Tool Call
5
+ </span>
6
+ </div>
7
+
8
+ <pre class="whitespace-pre-wrap text-gray-700 text-sm overflow-x-auto"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
9
+
10
+ <div>
11
+ <span class="text-sm text-gray-600"><%%= tool_calls.created_at&.strftime("%I:%M %p") %></span>
12
+ </div>
13
+ </div>
@@ -0,0 +1,21 @@
1
+ <%% error_message = tool.tool_error_message %>
2
+ <%% if error_message.present? %>
3
+ <%%= render "<%= message_model_name.underscore.pluralize %>/error",
4
+ <%= message_model_name.demodulize.underscore %>: tool,
5
+ title: "Tool Result Error",
6
+ error_message: error_message %>
7
+ <%% else %>
8
+ <div id="<%= message_variable_name %>_<%%= tool.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
9
+ <div>
10
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
11
+ Tool
12
+ </span>
13
+ </div>
14
+
15
+ <pre class="whitespace-pre-wrap text-gray-700 text-sm overflow-x-auto"><%%= tool.content.presence || "(no output)" %></pre>
16
+
17
+ <div>
18
+ <span class="text-sm text-gray-600"><%%= tool.created_at&.strftime("%I:%M %p") %></span>
19
+ </div>
20
+ </div>
21
+ <%% end %>
@@ -0,0 +1,17 @@
1
+ <%% model_id = <%= model_model_name.demodulize.underscore %>.respond_to?(:model_id) ? <%= model_model_name.demodulize.underscore %>.model_id : <%= model_model_name.demodulize.underscore %>.id %>
2
+ <%% row_id = [<%= model_model_name.demodulize.underscore %>.provider, model_id].join("_").parameterize(separator: "_") %>
3
+ <tr id="model_<%%= row_id %>">
4
+ <td class="px-3 py-2 text-sm text-gray-700"><%%= <%= model_model_name.demodulize.underscore %>.provider_class&.name || <%= model_model_name.demodulize.underscore %>.provider %></td>
5
+ <td class="px-3 py-2 text-sm text-gray-900"><%%= <%= model_model_name.demodulize.underscore %>.respond_to?(:display_name) ? <%= model_model_name.demodulize.underscore %>.display_name : <%= model_model_name.demodulize.underscore %>.name %></td>
6
+ <td class="px-3 py-2 text-sm text-gray-700"><%%= number_with_delimiter(<%= model_model_name.demodulize.underscore %>.context_window) if <%= model_model_name.demodulize.underscore %>.context_window %></td>
7
+ <td class="px-3 py-2 text-sm text-gray-700">
8
+ <%% input = <%= model_model_name.demodulize.underscore %>.input_price_per_million %>
9
+ <%% output = <%= model_model_name.demodulize.underscore %>.output_price_per_million %>
10
+ <%% if input && output %>
11
+ $<%%= "%.2f" % input %> / $<%%= "%.2f" % output %>
12
+ <%% end %>
13
+ </td>
14
+ <td class="px-3 py-2 text-sm">
15
+ <%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path(model: model_id), class: "text-blue-600 hover:text-blue-500" %>
16
+ </td>
17
+ </tr>
@@ -0,0 +1,40 @@
1
+ <%% content_for :title, "<%= model_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"><%= model_model_name.pluralize %></h1>
10
+ <div class="flex items-center gap-2">
11
+ <%%= link_to "<%= chat_model_name.pluralize %>", <%= chat_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
+ <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium cursor-pointer" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div id="<%= model_table_name %>" class="mt-5 overflow-x-auto">
17
+ <%% if @<%= model_variable_name.pluralize %>.any? %>
18
+ <table class="min-w-full divide-y divide-gray-300 border border-gray-300">
19
+ <thead class="bg-gray-50">
20
+ <tr>
21
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Provider</th>
22
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Model</th>
23
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Context Window</th>
24
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">$/1M tokens (In/Out)</th>
25
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900"></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="divide-y divide-gray-200 bg-white">
29
+ <%% @<%= model_variable_name.pluralize %>.each do |model_info| %>
30
+ <%%= render "<%= model_model_name.underscore.pluralize %>/<%= model_model_name.demodulize.underscore %>",
31
+ <%= model_model_name.demodulize.underscore %>: model_info %>
32
+ <%% end %>
33
+ </tbody>
34
+ </table>
35
+ <%% else %>
36
+ <p class="text-center my-10">No chat models found.</p>
37
+ <%% end %>
38
+ </div>
39
+
40
+ </div>
@@ -0,0 +1,27 @@
1
+ <%% content_for :title, "Showing <%= model_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <div class="md:w-2/3 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
+ <h1 class="font-bold text-4xl">Showing <%= model_table_name.singularize.humanize.downcase %></h1>
9
+
10
+ <div class="my-5 space-y-3">
11
+ <p><strong class="block font-medium mb-1">Provider:</strong> <%%= @<%= model_variable_name %>.provider_class&.name || @<%= model_variable_name %>.provider %></p>
12
+ <p><strong class="block font-medium mb-1">Model:</strong> <%%= @<%= model_variable_name %>.name %></p>
13
+ <p><strong class="block font-medium mb-1">ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
14
+ <p><strong class="block font-medium mb-1">Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) if @<%= model_variable_name %>.context_window %></p>
15
+ <p><strong class="block font-medium mb-1">Max Output Tokens:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) if @<%= model_variable_name %>.max_output_tokens %></p>
16
+ </div>
17
+
18
+ <%% if @<%= model_variable_name %>.capabilities.any? %>
19
+ <div class="my-5">
20
+ <strong class="block font-medium mb-1">Capabilities:</strong>
21
+ <%%= @<%= model_variable_name %>.capabilities.join(", ") %>
22
+ </div>
23
+ <%% end %>
24
+
25
+ <%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %> with this model", new_<%= chat_variable_name %>_path(model: @<%= model_variable_name %>.model_id), 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" %>
26
+ <%%= link_to "All <%= model_table_name.humanize.downcase %>", <%= model_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>
@@ -1,7 +1,7 @@
1
1
  <div id="<%%= dom_id <%= chat_model_name.demodulize.underscore %> %>">
2
2
  <div>
3
3
  <strong>Model:</strong>
4
- <%%= <%= chat_model_name.demodulize.underscore %>.<%= model_table_name.singularize %>&.name || 'Default' %>
4
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= model_table_name.singularize %>&.label || default_model_display_name %>
5
5
  </div>
6
6
 
7
7
  <div>
@@ -13,4 +13,4 @@
13
13
  <strong>Created:</strong>
14
14
  <%%= <%= chat_model_name.demodulize.underscore %>.created_at.strftime("%B %d, %Y at %I:%M %p") %>
15
15
  </div>
16
- </div>
16
+ </div>
@@ -14,7 +14,7 @@
14
14
  <div>
15
15
  <%%= form.label :model, "Select AI model:", style: "display: block" %>
16
16
  <%%= form.select :model,
17
- options_for_select(<%= model_model_name %>.pluck(:name, :model_id).unshift(["Default (#{RubyLLM.config.default_model})", nil]), @selected_model),
17
+ options_for_select(@chat_models.map { |model| [model.label, model.id] }.unshift([default_model_display_name, nil]), @selected_model),
18
18
  {},
19
19
  style: "width: 100%; max-width: 600px; padding: 5px;" %>
20
20
  </div>
@@ -26,4 +26,4 @@
26
26
  <div>
27
27
  <%%= form.submit "Start new <%= chat_table_name.singularize.humanize.downcase %>" %>
28
28
  </div>
29
- <%% end %>
29
+ <%% end %>
@@ -1,16 +1,28 @@
1
- <p style="color: green"><%%= notice %></p>
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
2
4
 
3
5
  <%% content_for :title, "<%= chat_model_name.pluralize %>" %>
4
6
 
5
7
  <h1><%= chat_model_name.pluralize %></h1>
8
+ <p>
9
+ <%%= link_to "<%= model_model_name.pluralize %>", <%= model_table_name %>_path %>
10
+ </p>
6
11
 
7
12
  <div id="<%= chat_table_name %>">
8
- <%% @<%= chat_table_name %>.each do |<%= chat_variable_name %>| %>
9
- <%%= render <%= chat_variable_name %> %>
10
- <p>
11
- <%%= link_to "Show this <%= chat_table_name.singularize.humanize.downcase %>", <%= chat_variable_name %> %>
12
- </p>
13
+ <%% if @<%= chat_table_name %>.any? %>
14
+ <%% @<%= chat_table_name %>.each do |<%= chat_variable_name %>| %>
15
+ <%%= render <%= chat_variable_name %> %>
16
+ <p>
17
+ <%%= link_to "Show this <%= chat_table_name.singularize.humanize.downcase %>", <%= chat_variable_name %> %>
18
+ </p>
19
+ <p>
20
+ <%%= link_to "Destroy this <%= chat_table_name.singularize.humanize.downcase %>", <%= chat_variable_name %>, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
21
+ </p>
22
+ <%% end %>
23
+ <%% else %>
24
+ <p>No <%= chat_table_name.humanize.downcase %> found.</p>
13
25
  <%% end %>
14
26
  </div>
15
27
 
16
- <%%= link_to "New <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path %>
28
+ <%%= link_to "New <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path %>
@@ -8,4 +8,4 @@
8
8
 
9
9
  <div>
10
10
  <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
11
- </div>
11
+ </div>
@@ -1,4 +1,6 @@
1
- <p style="color: green"><%%= notice %></p>
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
2
4
 
3
5
  <%%= turbo_stream_from "<%= chat_variable_name %>_#{@<%= chat_variable_name %>.id}" %>
4
6
 
@@ -6,7 +8,7 @@
6
8
 
7
9
  <h1><%= chat_model_name %> <%%= @<%= chat_variable_name %>.id %></h1>
8
10
 
9
- <p>Using <strong><%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>.name %></strong></p>
11
+ <p>Using <strong><%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>&.label || default_model_display_name %></strong></p>
10
12
 
11
13
  <div id="<%= message_table_name %>">
12
14
  <%% @<%= chat_variable_name %>.<%= message_table_name %>.where.not(id: nil).each do |<%= message_variable_name %>| %>
@@ -20,4 +22,4 @@
20
22
 
21
23
  <div style="margin-top: 20px;">
22
24
  <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
23
- </div>
25
+ </div>
@@ -0,0 +1,9 @@
1
+ <%% assistant ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #28a745;">
4
+ <div style="font-weight: bold; margin-bottom: 5px;">Assistant</div>
5
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>_content" style="white-space: pre-wrap;"><%%= assistant.content %></div>
6
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
7
+ <%%= assistant.created_at&.strftime("%I:%M %p") %>
8
+ </div>
9
+ </div>
@@ -1 +1 @@
1
- <%%= content %>
1
+ <%%= content -%>
@@ -0,0 +1,8 @@
1
+ <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>" class="<%= message_variable_name %>"
2
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #dc2626; background: #fef2f2;">
3
+ <div style="font-weight: bold; margin-bottom: 5px; color: #b91c1c;"><%%= title.presence || "Error" %></div>
4
+ <pre style="white-space: pre-wrap; margin: 0; color: #7f1d1d;"><%%= error_message %></pre>
5
+ <div style="font-size: 0.85em; color: #7f1d1d; margin-top: 5px;">
6
+ <%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %>
7
+ </div>
8
+ </div>
@@ -18,4 +18,4 @@
18
18
  <div>
19
19
  <%%= form.submit "Send <%= message_table_name.singularize.humanize.downcase %>" %>
20
20
  </div>
21
- <%% end %>
21
+ <%% end %>
@@ -0,0 +1,6 @@
1
+ <%% system ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= system.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
4
+ <div style="font-weight: bold; margin-bottom: 5px; color: #374151;">System</div>
5
+ <div style="white-space: pre-wrap; color: #4b5563;"><%%= system.content %></div>
6
+ </div>
@@ -0,0 +1,2 @@
1
+ <%% tool ||= local_assigns[:message] %>
2
+ <%%= render tool_result_partial(tool), tool: tool %>
@@ -1,7 +1,4 @@
1
- <div style="display: flex; flex-direction: column; gap: 3px; align-items: flex-start; font-family: monospace;">
2
- <%% <%= message_model_name.demodulize.underscore %>.<%= tool_call_variable_name.pluralize %>.each do |tool_call| %>
3
- <div style="background: #eee; padding: 5px; border-radius: 4px;">
4
- <%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)
5
- </div>
6
- <%% end %>
7
- </div>
1
+ <%% tool_calls ||= local_assigns[:message] %>
2
+ <%% tool_calls.<%= tool_call_variable_name.pluralize %>.each do |tool_call| %>
3
+ <%%= render tool_call_partial(tool_call), tool_calls: tool_calls, tool_call: tool_call %>
4
+ <%% end %>
@@ -0,0 +1,9 @@
1
+ <%% user ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= user.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #007bff;">
4
+ <div style="font-weight: bold; margin-bottom: 5px;">User</div>
5
+ <div id="<%= message_variable_name %>_<%%= user.id %>_content" style="white-space: pre-wrap;"><%%= user.content %></div>
6
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
7
+ <%%= user.created_at&.strftime("%I:%M %p") %>
8
+ </div>
9
+ </div>
@@ -1,9 +1,7 @@
1
- <%%= turbo_stream.append "<%= message_table_name %>" do %>
2
- <%% @<%= chat_variable_name %>.<%= message_table_name %>.last(2).each do |<%= message_variable_name %>| %>
3
- <%%= render <%= message_variable_name %> %>
1
+ <%% if defined?(@<%= chat_variable_name %>) && @<%= chat_variable_name %>.present? %>
2
+ <%%= turbo_stream.replace "new_<%= message_variable_name %>" do %>
3
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form",
4
+ <%= chat_variable_name %>: @<%= chat_variable_name %>,
5
+ <%= message_variable_name %>: @<%= chat_variable_name %>.<%= message_table_name %>.build %>
4
6
  <%% end %>
5
7
  <%% end %>
6
-
7
- <%%= turbo_stream.replace "new_<%= message_variable_name %>" do %>
8
- <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= chat_variable_name %>.<%= message_table_name %>.build %>
9
- <%% end %>
@@ -0,0 +1,8 @@
1
+ <div id="<%= message_variable_name %>_<%%= tool_calls.id %>" class="<%= message_variable_name %>"
2
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
3
+ <div style="font-weight: bold; margin-bottom: 5px;">Tool Call</div>
4
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
5
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
6
+ <%%= tool_calls.created_at&.strftime("%I:%M %p") %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,16 @@
1
+ <%% error_message = tool.tool_error_message %>
2
+ <%% if error_message.present? %>
3
+ <%%= render "<%= message_model_name.underscore.pluralize %>/error",
4
+ <%= message_model_name.demodulize.underscore %>: tool,
5
+ title: "Tool Result Error",
6
+ error_message: error_message %>
7
+ <%% else %>
8
+ <div id="<%= message_variable_name %>_<%%= tool.id %>" class="<%= message_variable_name %>"
9
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
10
+ <div style="font-weight: bold; margin-bottom: 5px;">Tool</div>
11
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
12
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
13
+ <%%= tool.created_at&.strftime("%I:%M %p") %>
14
+ </div>
15
+ </div>
16
+ <%% end %>
@@ -1,16 +1,15 @@
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>
1
+ <%% model_id = <%= model_model_name.demodulize.underscore %>.respond_to?(:model_id) ? <%= model_model_name.demodulize.underscore %>.model_id : <%= model_model_name.demodulize.underscore %>.id %>
2
+ <%% row_id = [<%= model_model_name.demodulize.underscore %>.provider, model_id].join("_").parameterize(separator: "_") %>
3
+ <tr id="model_<%%= row_id %>">
4
+ <td><%%= <%= model_model_name.demodulize.underscore %>.provider_class&.name || <%= model_model_name.demodulize.underscore %>.provider %></td>
5
+ <td><%%= <%= model_model_name.demodulize.underscore %>.respond_to?(:display_name) ? <%= model_model_name.demodulize.underscore %>.display_name : <%= model_model_name.demodulize.underscore %>.name %></td>
4
6
  <td><%%= number_with_delimiter(<%= model_model_name.demodulize.underscore %>.context_window) if <%= model_model_name.demodulize.underscore %>.context_window %></td>
5
7
  <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 %>
8
+ <%% input = <%= model_model_name.demodulize.underscore %>.input_price_per_million %>
9
+ <%% output = <%= model_model_name.demodulize.underscore %>.output_price_per_million %>
10
+ <%% if input && output %>
11
+ $<%%= "%.2f" % input %> / $<%%= "%.2f" % output %>
12
12
  <%% end %>
13
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>
14
+ <td><%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path(model: model_id) %></td>
15
+ </tr>
@@ -1,28 +1,38 @@
1
- <p style="color: green"><%%= notice %></p>
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
2
4
 
3
5
  <%% content_for :title, "<%= model_model_name.pluralize %>" %>
4
6
 
5
7
  <h1><%= model_model_name.pluralize %></h1>
8
+ <p>
9
+ <%%= link_to "<%= chat_model_name.pluralize %>", <%= chat_table_name %>_path %>
10
+ </p>
6
11
 
7
12
  <p>
8
13
  <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post %>
9
14
  </p>
10
15
 
11
16
  <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>
17
+ <%% if @<%= model_variable_name.pluralize %>.any? %>
18
+ <table>
19
+ <thead>
20
+ <tr>
21
+ <th>Provider</th>
22
+ <th>Model</th>
23
+ <th>Context Window</th>
24
+ <th>$/1M tokens (In/Out)</th>
25
+ <th></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <%% @<%= model_variable_name.pluralize %>.each do |model_info| %>
30
+ <%%= render "<%= model_model_name.underscore.pluralize %>/<%= model_model_name.demodulize.underscore %>",
31
+ <%= model_model_name.demodulize.underscore %>: model_info %>
32
+ <%% end %>
33
+ </tbody>
34
+ </table>
35
+ <%% else %>
36
+ <p>No chat models found.</p>
37
+ <%% end %>
26
38
  </div>
27
-
28
- <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
@@ -3,7 +3,7 @@
3
3
  <h1><%%= @<%= model_variable_name %>.name %></h1>
4
4
 
5
5
  <p><strong>ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
6
- <p><strong>Provider:</strong> <%%= @<%= model_variable_name %>.provider %></p>
6
+ <p><strong>Provider:</strong> <%%= @<%= model_variable_name %>.provider_class&.name || @<%= model_variable_name %>.provider %></p>
7
7
  <p><strong>Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) %> tokens</p>
8
8
  <p><strong>Max Output:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) %> tokens</p>
9
9
 
@@ -13,6 +13,5 @@
13
13
 
14
14
  <p>
15
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>
16
+ <%%= link_to "All models", <%= model_table_name %>_path %>
17
+ </p>
@@ -53,9 +53,9 @@ module RubyLLM
53
53
  params = []
54
54
 
55
55
  add_association_params(params, :messages, message_table_name, message_model_name,
56
- owner_table: chat_table_name, plural: true)
56
+ owner_table: chat_table_name, owner_model_name: chat_model_name, plural: true)
57
57
  add_association_params(params, :model, model_table_name, model_model_name,
58
- owner_table: chat_table_name)
58
+ owner_table: chat_table_name, owner_model_name: chat_model_name)
59
59
 
60
60
  "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
61
61
  end
@@ -64,11 +64,11 @@ module RubyLLM
64
64
  params = []
65
65
 
66
66
  add_association_params(params, :chat, chat_table_name, chat_model_name,
67
- owner_table: message_table_name)
67
+ owner_table: message_table_name, owner_model_name: message_model_name)
68
68
  add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
69
- owner_table: message_table_name, plural: true)
69
+ owner_table: message_table_name, owner_model_name: message_model_name, plural: true)
70
70
  add_association_params(params, :model, model_table_name, model_model_name,
71
- owner_table: message_table_name)
71
+ owner_table: message_table_name, owner_model_name: message_model_name)
72
72
 
73
73
  "acts_as_message#{" #{params.join(', ')}" if params.any?}"
74
74
  end
@@ -77,7 +77,7 @@ module RubyLLM
77
77
  params = []
78
78
 
79
79
  add_association_params(params, :chats, chat_table_name, chat_model_name,
80
- owner_table: model_table_name, plural: true)
80
+ owner_table: model_table_name, owner_model_name: model_model_name, plural: true)
81
81
 
82
82
  "acts_as_model#{" #{params.join(', ')}" if params.any?}"
83
83
  end
@@ -86,7 +86,7 @@ module RubyLLM
86
86
  params = []
87
87
 
88
88
  add_association_params(params, :message, message_table_name, message_model_name,
89
- owner_table: tool_call_table_name)
89
+ owner_table: tool_call_table_name, owner_model_name: tool_call_model_name)
90
90
 
91
91
  "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
92
  end
@@ -145,22 +145,38 @@ module RubyLLM
145
145
 
146
146
  private
147
147
 
148
- def add_association_params(params, default_assoc, table_name, model_name, owner_table:, plural: false) # rubocop:disable Metrics/ParameterLists
148
+ # rubocop:disable Metrics/ParameterLists
149
+ def add_association_params(params, default_assoc, table_name, model_name,
150
+ owner_table:, owner_model_name:, plural: false)
149
151
  assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
150
-
151
- default_foreign_key = "#{default_assoc}_id"
152
- # has_many/has_one: foreign key is on the associated table pointing back to owner
153
- # belongs_to: foreign key is on the owner table pointing to associated table
154
- foreign_key = if plural || default_assoc.to_s.pluralize == default_assoc.to_s # has_many or has_one
155
- "#{owner_table.singularize}_id"
156
- else # belongs_to
157
- "#{table_name.singularize}_id"
158
- end
152
+ collection_association = collection_association?(default_assoc, plural)
153
+ foreign_key = inferred_foreign_key(table_name, owner_table, collection_association)
154
+ default_foreign_key = default_inferred_foreign_key(assoc, owner_model_name, collection_association)
159
155
 
160
156
  params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
161
157
  params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
162
158
  params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
163
159
  end
160
+ # rubocop:enable Metrics/ParameterLists
161
+
162
+ def collection_association?(default_assoc, plural)
163
+ plural || default_assoc.to_s.pluralize == default_assoc.to_s
164
+ end
165
+
166
+ def inferred_foreign_key(table_name, owner_table, collection_association)
167
+ return "#{table_name.singularize}_id" unless collection_association
168
+
169
+ "#{owner_table.singularize}_id"
170
+ end
171
+
172
+ # Rails default inference:
173
+ # belongs_to :assoc -> assoc_id
174
+ # has_many/has_one -> owner demodulized model name + _id
175
+ def default_inferred_foreign_key(association_name, owner_model_name, collection_association)
176
+ return "#{association_name}_id" unless collection_association
177
+
178
+ "#{owner_model_name.demodulize.underscore}_id"
179
+ end
164
180
 
165
181
  # Convert namespaced model names to proper table names
166
182
  # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")