ruby_llm_community 1.0.0 → 1.1.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -2
  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 +9 -3
  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 +9 -8
  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 +2 -5
  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 +108 -467
  33. data/lib/ruby_llm/active_record/acts_as_legacy.rb +403 -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 +72 -6
  38. data/lib/ruby_llm/attachment.rb +27 -0
  39. data/lib/ruby_llm/configuration.rb +6 -0
  40. data/lib/ruby_llm/image_attachment.rb +12 -3
  41. data/lib/ruby_llm/message.rb +1 -1
  42. data/lib/ruby_llm/mime_type.rb +4 -0
  43. data/lib/ruby_llm/model/info.rb +4 -0
  44. data/lib/ruby_llm/models.json +2640 -1756
  45. data/lib/ruby_llm/models.rb +5 -15
  46. data/lib/ruby_llm/provider.rb +6 -4
  47. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  48. data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
  49. data/lib/ruby_llm/providers/gemini/capabilities.rb +5 -0
  50. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  51. data/lib/ruby_llm/providers/gpustack/media.rb +1 -1
  52. data/lib/ruby_llm/providers/ollama/media.rb +1 -1
  53. data/lib/ruby_llm/providers/openai/media.rb +4 -4
  54. data/lib/ruby_llm/providers/openai/response.rb +7 -6
  55. data/lib/ruby_llm/providers/openai/response_media.rb +1 -1
  56. data/lib/ruby_llm/providers/openai/streaming.rb +14 -11
  57. data/lib/ruby_llm/providers/openai/tools.rb +11 -6
  58. data/lib/ruby_llm/providers/vertexai.rb +1 -1
  59. data/lib/ruby_llm/providers/xai/capabilities.rb +166 -0
  60. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  61. data/lib/ruby_llm/providers/xai/models.rb +48 -0
  62. data/lib/ruby_llm/providers/xai.rb +46 -0
  63. data/lib/ruby_llm/railtie.rb +20 -3
  64. data/lib/ruby_llm/stream_accumulator.rb +0 -4
  65. data/lib/ruby_llm/utils.rb +5 -9
  66. data/lib/ruby_llm/version.rb +1 -1
  67. data/lib/ruby_llm_community.rb +4 -3
  68. data/lib/tasks/models.rake +29 -5
  69. data/lib/tasks/ruby_llm.rake +15 -0
  70. data/lib/tasks/vcr.rake +2 -2
  71. metadata +33 -3
  72. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
  73. data/lib/generators/ruby_llm/install_generator.rb +0 -146
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d28b384f53784c1954147d9f217caff1325893f1853e6be63c60158d9c37b79
4
- data.tar.gz: 3c6e66e260c1300b3b20ae730ec3755fcca19be8afc8dee754e26cc82cbee7f4
3
+ metadata.gz: 91657c36438fabeadbfa5bc881be1d9c0750916511e54624627c11478a7c8603
4
+ data.tar.gz: 2cf908a30159a970a4a973356ae60ef3844ffb507fe0bf1a567c11d1ecfa1e46
5
5
  SHA512:
6
- metadata.gz: 568f6c9a3facf5e5f721ec6e3d44862eb0988529cf72625a88220fe80f00ba89f5602f0a178d0ead3cfc3c4accca3a4b56321a3a24f84883df16bd1c72432458
7
- data.tar.gz: e6336936468acfc005284a1e6f7cbffe58e55e85c760082af71569d9555371ed124b26fe0cc70d274124f4b80530881c012d10079145f0c90383f2d6e2c51ff7
6
+ metadata.gz: 4c1cd8a73e001af382db8868d2c1037efe9e0bc411a559fdc36daa383897f2ba33aa42c6a4630e619c152f6879fd1e4a62a10e1c9c3a89a6745934db4c4b080d
7
+ data.tar.gz: 9a4391b67d16d171c35e9def92c0fd42d059c072ad03f148c14d0238cb5c59e00775fd42133417f086203bad228fd7d7b4b149f4190e93d1afd9e2bf78179531
data/README.md CHANGED
@@ -9,7 +9,7 @@
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=7)](https://badge.fury.io/rb/ruby_llm)
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=9)](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
15
  [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
@@ -17,6 +17,9 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
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.
@@ -27,6 +30,21 @@ Every AI provider ships their own bloated client. Different APIs. Different resp
27
30
 
28
31
  RubyLLM gives you one beautiful API for all of them. Same interface whether you're using GPT, Claude, or your local Ollama. Just three dependencies: [Faraday](https://github.com/lostisland/faraday), [Zeitwerk](https://github.com/fxn/zeitwerk), and [Marcel](https://github.com/rails/marcel). That's it.
29
32
 
33
+ ## How does RubyLLM Community differ?
34
+
35
+ Use this gem to get early access to features currently in PR at the main gem.
36
+
37
+ Some examples --
38
+
39
+ - Prompt caching for Anthropic
40
+ - Image editing for Gemini and OpenAI
41
+ - Responses API for OpenAI
42
+ - xAI provider that supports Grok 2, Grok 3, Grok 4, and Grok Code
43
+
44
+ This project is intended to be compatible with RubyLLM. I will attempt to keep it up to date as Carmine pushes commits often.
45
+
46
+ I may have to bump the major version if I have to break compatibility due to a change in direction on one of the PRs. I will provide instructions on how to upgrade in the release notes.
47
+
30
48
  ## Show me the code
31
49
 
32
50
  ```ruby
@@ -38,6 +56,7 @@ chat.ask "What's the best way to learn Ruby?"
38
56
  ```ruby
39
57
  # Analyze any file type
40
58
  chat.ask "What's in this image?", with: "ruby_conf.jpg"
59
+ chat.ask "What's happening in this video?", with: "video.mp4"
41
60
  chat.ask "Describe this meeting", with: "meeting.wav"
42
61
  chat.ask "Summarize this document", with: "contract.pdf"
43
62
  chat.ask "Explain this code", with: "app.rb"
@@ -97,7 +116,7 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
97
116
  ## Features
98
117
 
99
118
  * **Chat:** Conversational AI with `RubyLLM.chat`
100
- * **Vision:** Analyze images and screenshots
119
+ * **Vision:** Analyze images and videos
101
120
  * **Audio:** Transcribe and understand speech
102
121
  * **Documents:** Extract from PDFs, CSVs, JSON, any file type
103
122
  * **Image generation:** Create images with `RubyLLM.paint`
@@ -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