ruby_llm_swarm 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +175 -0
  4. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  22. data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
  23. data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
  24. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  25. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  26. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
  27. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  28. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
  29. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
  30. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
  31. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  32. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  33. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  34. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  35. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
  36. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  37. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  38. data/lib/ruby_llm/active_record/acts_as.rb +174 -0
  39. data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
  40. data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
  41. data/lib/ruby_llm/active_record/message_methods.rb +81 -0
  42. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  43. data/lib/ruby_llm/aliases.json +295 -0
  44. data/lib/ruby_llm/aliases.rb +38 -0
  45. data/lib/ruby_llm/attachment.rb +220 -0
  46. data/lib/ruby_llm/chat.rb +816 -0
  47. data/lib/ruby_llm/chunk.rb +6 -0
  48. data/lib/ruby_llm/configuration.rb +78 -0
  49. data/lib/ruby_llm/connection.rb +126 -0
  50. data/lib/ruby_llm/content.rb +73 -0
  51. data/lib/ruby_llm/context.rb +29 -0
  52. data/lib/ruby_llm/embedding.rb +29 -0
  53. data/lib/ruby_llm/error.rb +84 -0
  54. data/lib/ruby_llm/image.rb +49 -0
  55. data/lib/ruby_llm/message.rb +86 -0
  56. data/lib/ruby_llm/mime_type.rb +71 -0
  57. data/lib/ruby_llm/model/info.rb +111 -0
  58. data/lib/ruby_llm/model/modalities.rb +22 -0
  59. data/lib/ruby_llm/model/pricing.rb +48 -0
  60. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  61. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  62. data/lib/ruby_llm/model.rb +7 -0
  63. data/lib/ruby_llm/models.json +33198 -0
  64. data/lib/ruby_llm/models.rb +231 -0
  65. data/lib/ruby_llm/models_schema.json +168 -0
  66. data/lib/ruby_llm/moderation.rb +56 -0
  67. data/lib/ruby_llm/provider.rb +243 -0
  68. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  69. data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
  70. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  71. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  72. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  73. data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
  74. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  75. data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
  76. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  77. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  78. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  79. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  80. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  81. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  82. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  83. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  84. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  85. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
  86. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  87. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  88. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  89. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  90. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  91. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  92. data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
  93. data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
  94. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  95. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  96. data/lib/ruby_llm/providers/gemini/media.rb +112 -0
  97. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  98. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  99. data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
  100. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  101. data/lib/ruby_llm/providers/gemini.rb +37 -0
  102. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  103. data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
  104. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  105. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  106. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  107. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  108. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  109. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  110. data/lib/ruby_llm/providers/mistral.rb +32 -0
  111. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  112. data/lib/ruby_llm/providers/ollama/media.rb +46 -0
  113. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  114. data/lib/ruby_llm/providers/ollama.rb +30 -0
  115. data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
  116. data/lib/ruby_llm/providers/openai/chat.rb +88 -0
  117. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  118. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  119. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  120. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  121. data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
  122. data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
  123. data/lib/ruby_llm/providers/openai/tools.rb +98 -0
  124. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  125. data/lib/ruby_llm/providers/openai.rb +44 -0
  126. data/lib/ruby_llm/providers/openai_responses.rb +395 -0
  127. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  128. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  129. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  130. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  131. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  132. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  133. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  134. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  135. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  136. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  137. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  138. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  139. data/lib/ruby_llm/railtie.rb +35 -0
  140. data/lib/ruby_llm/responses_session.rb +77 -0
  141. data/lib/ruby_llm/stream_accumulator.rb +101 -0
  142. data/lib/ruby_llm/streaming.rb +153 -0
  143. data/lib/ruby_llm/tool.rb +209 -0
  144. data/lib/ruby_llm/tool_call.rb +22 -0
  145. data/lib/ruby_llm/tool_executors.rb +125 -0
  146. data/lib/ruby_llm/transcription.rb +35 -0
  147. data/lib/ruby_llm/utils.rb +91 -0
  148. data/lib/ruby_llm/version.rb +5 -0
  149. data/lib/ruby_llm.rb +140 -0
  150. data/lib/tasks/models.rake +525 -0
  151. data/lib/tasks/release.rake +67 -0
  152. data/lib/tasks/ruby_llm.rake +15 -0
  153. data/lib/tasks/vcr.rake +92 -0
  154. metadata +346 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b929914f2235d8170762331427f953b7e43d718d8372fdbe73e559ad1d5fbdf
4
+ data.tar.gz: 87112298cccfc79b10af119e18d5bbfa0171a165bae5ab7e218bab376b526205
5
+ SHA512:
6
+ metadata.gz: da232b16c619e4a4eea8b168201a82769abd8c7871f3a0cc4b1b729618e6b830828f592a750cf1bfb0be9cfee72477c51a6298d815d1ce3c5cea78fb9612c644
7
+ data.tar.gz: 378c4ed4fbc8a9969fb35af754672bab0d77ffd4340040ca3b08ca1385a457045ee70858b57fa74e4aca346ed79f26bbf4207e9ea444e2e33a06f530ed15125b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carmine Paolino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="/docs/assets/images/logotype_dark.svg">
5
+ <img src="/docs/assets/images/logotype.svg" alt="RubyLLM" height="120" width="250">
6
+ </picture>
7
+
8
+ <strong>One *beautiful* Ruby API for GPT, Claude, Gemini, and more.</strong>
9
+
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
+
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=10)](https://badge.fury.io/rb/ruby_llm)
13
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
14
+ [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm)](https://rubygems.org/gems/ruby_llm)
15
+ [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
16
+
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
+ </div>
19
+
20
+ > [!NOTE]
21
+ > Using RubyLLM? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
22
+
23
+ ---
24
+
25
+ Build chatbots, AI agents, RAG applications. Works with OpenAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
26
+
27
+ ## Why RubyLLM?
28
+
29
+ Every AI provider ships their own bloated client. Different APIs. Different response formats. Different conventions. It's exhausting.
30
+
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, Zeitwerk, and Marcel. That's it.
32
+
33
+ ## Show me the code
34
+
35
+ ```ruby
36
+ # Just ask questions
37
+ chat = RubyLLM.chat
38
+ chat.ask "What's the best way to learn Ruby?"
39
+ ```
40
+
41
+ ```ruby
42
+ # Analyze any file type
43
+ chat.ask "What's in this image?", with: "ruby_conf.jpg"
44
+ chat.ask "What's happening in this video?", with: "video.mp4"
45
+ chat.ask "Describe this meeting", with: "meeting.wav"
46
+ chat.ask "Summarize this document", with: "contract.pdf"
47
+ chat.ask "Explain this code", with: "app.rb"
48
+ ```
49
+
50
+ ```ruby
51
+ # Multiple files at once
52
+ chat.ask "Analyze these files", with: ["diagram.png", "report.pdf", "notes.txt"]
53
+ ```
54
+
55
+ ```ruby
56
+ # Stream responses
57
+ chat.ask "Tell me a story about Ruby" do |chunk|
58
+ print chunk.content
59
+ end
60
+ ```
61
+
62
+ ```ruby
63
+ # Generate images
64
+ RubyLLM.paint "a sunset over mountains in watercolor style"
65
+ ```
66
+
67
+ ```ruby
68
+ # Create embeddings
69
+ RubyLLM.embed "Ruby is elegant and expressive"
70
+ ```
71
+
72
+ ```ruby
73
+ # Transcribe audio to text
74
+ RubyLLM.transcribe "meeting.wav"
75
+ ```
76
+
77
+ ```ruby
78
+ # Moderate content for safety
79
+ RubyLLM.moderate "Check if this text is safe"
80
+ ```
81
+
82
+ ```ruby
83
+ # Let AI use your code
84
+ class Weather < RubyLLM::Tool
85
+ description "Get current weather"
86
+ param :latitude
87
+ param :longitude
88
+
89
+ def execute(latitude:, longitude:)
90
+ url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
91
+ JSON.parse(Faraday.get(url).body)
92
+ end
93
+ end
94
+
95
+ chat.with_tool(Weather).ask "What's the weather in Berlin?"
96
+ ```
97
+
98
+ ```ruby
99
+ # Get structured output
100
+ class ProductSchema < RubyLLM::Schema
101
+ string :name
102
+ number :price
103
+ array :features do
104
+ string
105
+ end
106
+ end
107
+
108
+ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "product.txt"
109
+ ```
110
+
111
+ ## Features
112
+
113
+ * **Chat:** Conversational AI with `RubyLLM.chat`
114
+ * **Vision:** Analyze images and videos
115
+ * **Audio:** Transcribe and understand speech with `RubyLLM.transcribe`
116
+ * **Documents:** Extract from PDFs, CSVs, JSON, any file type
117
+ * **Image generation:** Create images with `RubyLLM.paint`
118
+ * **Embeddings:** Generate embeddings with `RubyLLM.embed`
119
+ * **Moderation:** Content safety with `RubyLLM.moderate`
120
+ * **Tools:** Let AI call your Ruby methods
121
+ * **Structured output:** JSON schemas that just work
122
+ * **Streaming:** Real-time responses with blocks
123
+ * **Rails:** ActiveRecord integration with `acts_as_chat`
124
+ * **Async:** Fiber-based concurrency
125
+ * **Model registry:** 500+ models with capability detection and pricing
126
+ * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
127
+
128
+ ## Installation
129
+
130
+ Add to your Gemfile:
131
+ ```ruby
132
+ gem 'ruby_llm'
133
+ ```
134
+ Then `bundle install`.
135
+
136
+ Configure your API keys:
137
+ ```ruby
138
+ # config/initializers/ruby_llm.rb
139
+ RubyLLM.configure do |config|
140
+ config.openai_api_key = ENV['OPENAI_API_KEY']
141
+ end
142
+ ```
143
+
144
+ ## Rails
145
+
146
+ ```bash
147
+ # Install Rails Integration
148
+ rails generate ruby_llm:install
149
+
150
+ # Add Chat UI (optional)
151
+ rails generate ruby_llm:chat_ui
152
+ ```
153
+
154
+ ```ruby
155
+ class Chat < ApplicationRecord
156
+ acts_as_chat
157
+ end
158
+
159
+ chat = Chat.create! model: "claude-sonnet-4"
160
+ chat.ask "What's in this file?", with: "report.pdf"
161
+ ```
162
+
163
+ Visit `http://localhost:3000/chats` for a ready-to-use chat interface!
164
+
165
+ ## Documentation
166
+
167
+ [rubyllm.com](https://rubyllm.com)
168
+
169
+ ## Contributing
170
+
171
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
172
+
173
+ ## License
174
+
175
+ Released under the MIT License.
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require_relative '../generator_helpers'
5
+
6
+ module RubyLLM
7
+ module Generators
8
+ # Generates a simple chat UI scaffold for RubyLLM
9
+ class ChatUIGenerator < Rails::Generators::Base
10
+ include RubyLLM::Generators::GeneratorHelpers
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+
14
+ namespace 'ruby_llm:chat_ui'
15
+
16
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
17
+
18
+ desc 'Creates a chat UI scaffold with Turbo streaming\n' \
19
+ 'Usage: rails g ruby_llm:chat_ui [chat:ChatName] [message:MessageName] ...'
20
+
21
+ def check_model_exists
22
+ model_path = "app/models/#{message_model_name.underscore}.rb"
23
+ return if File.exist?(model_path)
24
+
25
+ # Build the argument string for the install/upgrade commands
26
+ args = []
27
+ args << "chat:#{chat_model_name}" if chat_model_name != 'Chat'
28
+ args << "message:#{message_model_name}" if message_model_name != 'Message'
29
+ args << "model:#{model_model_name}" if model_model_name != 'Model'
30
+ args << "tool_call:#{tool_call_model_name}" if tool_call_model_name != 'ToolCall'
31
+ arg_string = args.any? ? " #{args.join(' ')}" : ''
32
+
33
+ raise Thor::Error, <<~ERROR
34
+ Model file not found: #{model_path}
35
+
36
+ Please run the install generator first:
37
+ rails generate ruby_llm:install#{arg_string}
38
+
39
+ Or if upgrading from <= 1.6.x, run the upgrade generator:
40
+ rails generate ruby_llm:upgrade_to_v1_7#{arg_string}
41
+ ERROR
42
+ end
43
+
44
+ def create_views
45
+ # For namespaced models, use the proper Rails convention path
46
+ chat_view_path = chat_model_name.underscore.pluralize
47
+ message_view_path = message_model_name.underscore.pluralize
48
+ model_view_path = model_model_name.underscore.pluralize
49
+
50
+ # 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',
55
+ "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"
57
+
58
+ # 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',
62
+ "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"
66
+
67
+ # 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',
71
+ "app/views/#{model_view_path}/_#{model_model_name.demodulize.underscore}.html.erb"
72
+ end
73
+
74
+ def create_controllers
75
+ # For namespaced models, use the proper Rails convention path
76
+ chat_controller_path = chat_model_name.underscore.pluralize
77
+ message_controller_path = message_model_name.underscore.pluralize
78
+ model_controller_path = model_model_name.underscore.pluralize
79
+
80
+ template 'controllers/chats_controller.rb', "app/controllers/#{chat_controller_path}_controller.rb"
81
+ template 'controllers/messages_controller.rb', "app/controllers/#{message_controller_path}_controller.rb"
82
+ template 'controllers/models_controller.rb', "app/controllers/#{model_controller_path}_controller.rb"
83
+ end
84
+
85
+ def create_jobs
86
+ template 'jobs/chat_response_job.rb', "app/jobs/#{variable_name_for(chat_model_name)}_response_job.rb"
87
+ end
88
+
89
+ def add_routes
90
+ # For namespaced models, use Rails convention with namespace blocks
91
+ if chat_model_name.include?('::')
92
+ namespace = chat_model_name.deconstantize.underscore
93
+ chat_resource = chat_model_name.demodulize.underscore.pluralize
94
+ message_resource = message_model_name.demodulize.underscore.pluralize
95
+ model_resource = model_model_name.demodulize.underscore.pluralize
96
+
97
+ routes_content = <<~ROUTES.strip
98
+ namespace :#{namespace} do
99
+ resources :#{model_resource}, only: [:index, :show] do
100
+ collection do
101
+ post :refresh
102
+ end
103
+ end
104
+ resources :#{chat_resource} do
105
+ resources :#{message_resource}, only: [:create]
106
+ end
107
+ end
108
+ ROUTES
109
+ route routes_content
110
+ else
111
+ model_routes = <<~ROUTES.strip
112
+ resources :#{model_table_name}, only: [:index, :show] do
113
+ collection do
114
+ post :refresh
115
+ end
116
+ end
117
+ ROUTES
118
+ route model_routes
119
+ chat_routes = <<~ROUTES.strip
120
+ resources :#{chat_table_name} do
121
+ resources :#{message_table_name}, only: [:create]
122
+ end
123
+ ROUTES
124
+ route chat_routes
125
+ end
126
+ end
127
+
128
+ def add_broadcasting_to_message_model
129
+ msg_var = variable_name_for(message_model_name)
130
+ chat_var = variable_name_for(chat_model_name)
131
+ msg_path = message_model_name.underscore
132
+
133
+ # For namespaced models, we need the association name which might be different
134
+ # e.g., for LLM::Message, the chat association might be :llm_chat
135
+ chat_association = chat_table_name.singularize
136
+
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
151
+
152
+ broadcast_append_chunk_method = <<-RUBY
153
+
154
+ def broadcast_append_chunk(content)
155
+ broadcast_append_to "#{chat_var}_\#{#{chat_association}_id}",
156
+ target: "#{msg_var}_\#{id}_content",
157
+ partial: "#{partial_path}/content",
158
+ locals: { content: content }
159
+ end
160
+ RUBY
161
+
162
+ inject_into_file "app/models/#{msg_path}.rb", before: "end\n" do
163
+ " #{broadcasting_code}\n#{broadcast_append_chunk_method}"
164
+ end
165
+ rescue Errno::ENOENT
166
+ 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
169
+ end
170
+
171
+ def display_post_install_message
172
+ return unless behavior == :invoke
173
+
174
+ # Show the correct URL based on whether models are namespaced
175
+ url_path = if chat_model_name.include?('::')
176
+ chat_model_name.underscore.pluralize
177
+ else
178
+ chat_table_name
179
+ end
180
+
181
+ say "\n ✅ Chat UI installed!", :green
182
+ say "\n Start your server and visit http://localhost:3000/#{url_path}", :cyan
183
+ say "\n"
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,39 @@
1
+ class <%= chat_controller_class_name %> < ApplicationController
2
+ before_action :set_<%= chat_variable_name %>, only: [:show]
3
+
4
+ def index
5
+ @<%= chat_table_name %> = <%= chat_model_name %>.order(created_at: :desc)
6
+ end
7
+
8
+ def new
9
+ @<%= chat_variable_name %> = <%= chat_model_name %>.new
10
+ @selected_model = params[:model]
11
+ end
12
+
13
+ def create
14
+ return unless prompt.present?
15
+
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
+ end
21
+
22
+ def show
23
+ @<%= message_variable_name %> = @<%= chat_variable_name %>.<%= message_table_name %>.build
24
+ end
25
+
26
+ private
27
+
28
+ def set_<%= chat_variable_name %>
29
+ @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:id])
30
+ 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
@@ -0,0 +1,24 @@
1
+ class <%= message_controller_class_name %> < ApplicationController
2
+ before_action :set_<%= chat_variable_name %>
3
+
4
+ def create
5
+ return unless content.present?
6
+
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 %> }
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def set_<%= chat_variable_name %>
18
+ @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:<%= chat_model_name.include?('::') ? chat_model_name.demodulize.underscore : chat_variable_name %>_id])
19
+ end
20
+
21
+ def content
22
+ params[:<%= message_variable_name %>][:content]
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ class <%= model_controller_class_name %> < ApplicationController
2
+ def index
3
+ @<%= model_table_name %> = <%= model_model_name %>.all
4
+ end
5
+
6
+ def show
7
+ @<%= model_variable_name %> = <%= model_model_name %>.find(params[:id])
8
+ end
9
+
10
+ def refresh
11
+ <%= model_model_name %>.refresh!
12
+ redirect_to <%= model_table_name %>_path, notice: "<%= model_model_name.pluralize %> refreshed successfully"
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ class <%= chat_job_class_name %> < ApplicationJob
2
+ def perform(<%= chat_variable_name %>_id, content)
3
+ <%= chat_variable_name %> = <%= chat_model_name %>.find(<%= chat_variable_name %>_id)
4
+
5
+ <%= chat_variable_name %>.ask(content) do |chunk|
6
+ if chunk.content && !chunk.content.blank?
7
+ <%= message_variable_name %> = <%= chat_variable_name %>.<%= message_table_name %>.last
8
+ <%= message_variable_name %>.broadcast_append_chunk(chunk.content)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ <div id="<%%= dom_id <%= chat_model_name.demodulize.underscore %> %>">
2
+ <div>
3
+ <strong>Model:</strong>
4
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= model_table_name.singularize %>&.name || 'Default' %>
5
+ </div>
6
+
7
+ <div>
8
+ <strong>Messages:</strong>
9
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= message_table_name %>.count %>
10
+ </div>
11
+
12
+ <div>
13
+ <strong>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,29 @@
1
+ <%%= form_with(model: <%= chat_variable_name %>, url: <%= chat_table_name %>_path) do |form| %>
2
+ <%% if <%= chat_variable_name %>.errors.any? %>
3
+ <div style="color: red">
4
+ <h2><%%= pluralize(<%= chat_variable_name %>.errors.count, "error") %> prohibited this <%= chat_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul>
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>
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_table_name.singularize.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_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
+ <%% end %>
14
+ </div>
15
+
16
+ <%%= link_to "New <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path %>
@@ -0,0 +1,11 @@
1
+ <%% content_for :title, "New <%= chat_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <h1>New <%= chat_table_name.singularize.humanize.downcase %></h1>
4
+
5
+ <%%= render "form", <%= chat_variable_name %>: @<%= chat_variable_name %> %>
6
+
7
+ <br>
8
+
9
+ <div>
10
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
11
+ </div>
@@ -0,0 +1,23 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <%%= turbo_stream_from "<%= chat_variable_name %>_#{@<%= chat_variable_name %>.id}" %>
4
+
5
+ <%% content_for :title, "<%= chat_model_name %>" %>
6
+
7
+ <h1><%= chat_model_name %> <%%= @<%= chat_variable_name %>.id %></h1>
8
+
9
+ <p>Using <strong><%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>.name %></strong></p>
10
+
11
+ <div id="<%= message_table_name %>">
12
+ <%% @<%= chat_variable_name %>.<%= message_table_name %>.where.not(id: nil).each do |<%= message_variable_name %>| %>
13
+ <%%= render <%= message_variable_name %> %>
14
+ <%% end %>
15
+ </div>
16
+
17
+ <div style="margin-top: 30px;">
18
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= message_variable_name %> %>
19
+ </div>
20
+
21
+ <div style="margin-top: 20px;">
22
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
23
+ </div>
@@ -0,0 +1,21 @@
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 %>") do |form| %>
2
+ <%% if <%= message_variable_name %>.errors.any? %>
3
+ <div style="color: red">
4
+ <h2><%%= pluralize(<%= message_variable_name %>.errors.count, "error") %> prohibited this <%= message_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul>
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>
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_table_name.singularize.humanize.downcase %>" %>
20
+ </div>
21
+ <%% end %>
@@ -0,0 +1,13 @@
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 <%%= <%= message_model_name.demodulize.underscore %>.role == 'user' ? '#007bff' : '#28a745' %>">
3
+ <div style="font-weight: bold; margin-bottom: 5px;">
4
+ <%%= <%= message_model_name.demodulize.underscore %>.role&.capitalize %>
5
+ </div>
6
+ <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>_content" style="white-space: pre-wrap;"><%%= <%= message_model_name.demodulize.underscore %>.content %></div>
7
+ <%% if <%= message_model_name.demodulize.underscore %>.tool_call? %>
8
+ <%%= render "<%= message_model_name.underscore.pluralize %>/tool_calls", <%= message_model_name.demodulize.underscore %>: <%= message_model_name.demodulize.underscore %> %>
9
+ <%% end %>
10
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
11
+ <%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %>
12
+ </div>
13
+ </div>
@@ -0,0 +1,7 @@
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>
@@ -0,0 +1,9 @@
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 %> %>
4
+ <%% end %>
5
+ <%% 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 %>