lex-llm 0.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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/dependabot.yml +18 -0
  4. data/.github/workflows/ci.yml +16 -0
  5. data/.gitignore +19 -0
  6. data/.rubocop.yml +42 -0
  7. data/CHANGELOG.md +15 -0
  8. data/Gemfile +50 -0
  9. data/LICENSE +21 -0
  10. data/README.md +279 -0
  11. data/lex-llm.gemspec +43 -0
  12. data/lib/generators/lex_llm/agent/agent_generator.rb +36 -0
  13. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +6 -0
  14. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  15. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +256 -0
  16. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +38 -0
  17. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +21 -0
  18. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  19. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  20. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  21. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  22. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  23. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  24. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  25. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  26. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  27. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  28. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  29. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  30. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  31. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  32. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  33. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  34. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  35. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  36. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  37. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  38. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  39. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  40. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  41. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +28 -0
  42. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  43. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +25 -0
  44. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  45. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  46. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  47. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  48. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  49. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  50. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -0
  51. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  52. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +7 -0
  53. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  54. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  55. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +15 -0
  56. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +38 -0
  57. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +17 -0
  58. data/lib/generators/lex_llm/generator_helpers.rb +214 -0
  59. data/lib/generators/lex_llm/install/install_generator.rb +109 -0
  60. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  61. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +3 -0
  62. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +7 -0
  63. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +19 -0
  64. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +39 -0
  65. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +21 -0
  66. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +20 -0
  67. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +4 -0
  68. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +3 -0
  69. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +3 -0
  70. data/lib/generators/lex_llm/schema/schema_generator.rb +26 -0
  71. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +2 -0
  72. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +9 -0
  73. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +13 -0
  74. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +13 -0
  75. data/lib/generators/lex_llm/tool/tool_generator.rb +96 -0
  76. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  77. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  78. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  79. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  80. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  81. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +122 -0
  82. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  83. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  84. data/lib/legion/extensions/llm/provider_settings.rb +49 -0
  85. data/lib/legion/extensions/llm/transport/fleet_lane.rb +70 -0
  86. data/lib/legion/extensions/llm.rb +50 -0
  87. data/lib/lex_llm/active_record/acts_as.rb +180 -0
  88. data/lib/lex_llm/active_record/acts_as_legacy.rb +503 -0
  89. data/lib/lex_llm/active_record/chat_methods.rb +468 -0
  90. data/lib/lex_llm/active_record/message_methods.rb +131 -0
  91. data/lib/lex_llm/active_record/model_methods.rb +76 -0
  92. data/lib/lex_llm/active_record/payload_helpers.rb +26 -0
  93. data/lib/lex_llm/active_record/tool_call_methods.rb +15 -0
  94. data/lib/lex_llm/agent.rb +365 -0
  95. data/lib/lex_llm/aliases.json +436 -0
  96. data/lib/lex_llm/aliases.rb +38 -0
  97. data/lib/lex_llm/attachment.rb +223 -0
  98. data/lib/lex_llm/chat.rb +351 -0
  99. data/lib/lex_llm/chunk.rb +6 -0
  100. data/lib/lex_llm/configuration.rb +81 -0
  101. data/lib/lex_llm/connection.rb +130 -0
  102. data/lib/lex_llm/content.rb +77 -0
  103. data/lib/lex_llm/context.rb +29 -0
  104. data/lib/lex_llm/embedding.rb +29 -0
  105. data/lib/lex_llm/error.rb +112 -0
  106. data/lib/lex_llm/image.rb +105 -0
  107. data/lib/lex_llm/message.rb +107 -0
  108. data/lib/lex_llm/mime_type.rb +71 -0
  109. data/lib/lex_llm/model/info.rb +113 -0
  110. data/lib/lex_llm/model/modalities.rb +22 -0
  111. data/lib/lex_llm/model/pricing.rb +48 -0
  112. data/lib/lex_llm/model/pricing_category.rb +46 -0
  113. data/lib/lex_llm/model/pricing_tier.rb +33 -0
  114. data/lib/lex_llm/model.rb +7 -0
  115. data/lib/lex_llm/models.json +57241 -0
  116. data/lib/lex_llm/models.rb +506 -0
  117. data/lib/lex_llm/models_schema.json +168 -0
  118. data/lib/lex_llm/moderation.rb +56 -0
  119. data/lib/lex_llm/provider.rb +278 -0
  120. data/lib/lex_llm/railtie.rb +35 -0
  121. data/lib/lex_llm/routing/lane_key.rb +51 -0
  122. data/lib/lex_llm/routing/model_offering.rb +169 -0
  123. data/lib/lex_llm/routing.rb +7 -0
  124. data/lib/lex_llm/stream_accumulator.rb +203 -0
  125. data/lib/lex_llm/streaming.rb +175 -0
  126. data/lib/lex_llm/thinking.rb +49 -0
  127. data/lib/lex_llm/tokens.rb +47 -0
  128. data/lib/lex_llm/tool.rb +254 -0
  129. data/lib/lex_llm/tool_call.rb +25 -0
  130. data/lib/lex_llm/transcription.rb +35 -0
  131. data/lib/lex_llm/utils.rb +91 -0
  132. data/lib/lex_llm/version.rb +5 -0
  133. data/lib/lex_llm.rb +95 -0
  134. data/lib/tasks/lex_llm.rake +23 -0
  135. metadata +349 -0
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require_relative '../generator_helpers'
5
+
6
+ module LexLLM
7
+ module Generators
8
+ # Generates a simple chat UI scaffold for LexLLM
9
+ class ChatUIGenerator < Rails::Generators::Base
10
+ include LexLLM::Generators::GeneratorHelpers
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+
14
+ namespace 'lex_llm:chat_ui'
15
+
16
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
17
+ class_option :ui, type: :string, default: 'auto', enum: %w[scaffold tailwind auto],
18
+ desc: 'UI template style (scaffold, tailwind, auto)'
19
+
20
+ desc 'Creates a chat UI scaffold with Turbo streaming\n' \
21
+ 'Usage: bin/rails g lex_llm:chat_ui [chat:ChatName] [message:MessageName] ...'
22
+
23
+ def check_model_exists
24
+ model_path = "app/models/#{message_model_name.underscore}.rb"
25
+ return if File.exist?(model_path)
26
+
27
+ # Build the argument string for the install/upgrade commands
28
+ args = []
29
+ args << "chat:#{chat_model_name}" if chat_model_name != 'Chat'
30
+ args << "message:#{message_model_name}" if message_model_name != 'Message'
31
+ args << "model:#{model_model_name}" if model_model_name != 'Model'
32
+ args << "tool_call:#{tool_call_model_name}" if tool_call_model_name != 'ToolCall'
33
+ arg_string = args.any? ? " #{args.join(' ')}" : ''
34
+
35
+ raise Thor::Error, <<~ERROR
36
+ Model file not found: #{model_path}
37
+
38
+ Please run the install generator first:
39
+ bin/rails generate lex_llm:install#{arg_string}
40
+
41
+ Or if upgrading from <= 1.6.x, run the upgrade generator:
42
+ bin/rails generate lex_llm:upgrade_to_v1_7#{arg_string}
43
+ ERROR
44
+ end
45
+
46
+ def create_views
47
+ # Design contract:
48
+ # - `scaffold` should stay close to Rails scaffold ERB output.
49
+ # - `tailwind` should stay close to tailwindcss-rails scaffold output.
50
+ # - Only small chat-specific affordances should be layered on top.
51
+ # For namespaced models, use the proper Rails convention path
52
+ chat_view_path = chat_model_name.underscore.pluralize
53
+ message_view_path = message_model_name.underscore.pluralize
54
+ model_view_path = model_model_name.underscore.pluralize
55
+
56
+ # Chat views
57
+ template ui_template('views/chats/index.html.erb'), "app/views/#{chat_view_path}/index.html.erb"
58
+ template ui_template('views/chats/new.html.erb'), "app/views/#{chat_view_path}/new.html.erb"
59
+ template ui_template('views/chats/show.html.erb'), "app/views/#{chat_view_path}/show.html.erb"
60
+ template ui_template('views/chats/_chat.html.erb'),
61
+ "app/views/#{chat_view_path}/_#{chat_model_name.demodulize.underscore}.html.erb"
62
+ template ui_template('views/chats/_form.html.erb'), "app/views/#{chat_view_path}/_form.html.erb"
63
+
64
+ # Message views
65
+ template ui_template('views/messages/_assistant.html.erb'), "app/views/#{message_view_path}/_assistant.html.erb"
66
+ template ui_template('views/messages/_user.html.erb'), "app/views/#{message_view_path}/_user.html.erb"
67
+ template ui_template('views/messages/_system.html.erb'), "app/views/#{message_view_path}/_system.html.erb"
68
+ template ui_template('views/messages/_tool.html.erb'), "app/views/#{message_view_path}/_tool.html.erb"
69
+ template ui_template('views/messages/_error.html.erb'), "app/views/#{message_view_path}/_error.html.erb"
70
+ template ui_template('views/messages/_tool_calls.html.erb'),
71
+ "app/views/#{message_view_path}/_tool_calls.html.erb"
72
+ empty_directory "app/views/#{message_view_path}/tool_calls"
73
+ template ui_template('views/messages/tool_calls/_default.html.erb'),
74
+ "app/views/#{message_view_path}/tool_calls/_default.html.erb"
75
+ empty_directory "app/views/#{message_view_path}/tool_results"
76
+ template ui_template('views/messages/tool_results/_default.html.erb'),
77
+ "app/views/#{message_view_path}/tool_results/_default.html.erb"
78
+ template ui_template('views/messages/create.turbo_stream.erb'),
79
+ "app/views/#{message_view_path}/create.turbo_stream.erb"
80
+ template ui_template('views/messages/_content.html.erb'), "app/views/#{message_view_path}/_content.html.erb"
81
+ template ui_template('views/messages/_form.html.erb'), "app/views/#{message_view_path}/_form.html.erb"
82
+
83
+ # Model views
84
+ template ui_template('views/models/index.html.erb'), "app/views/#{model_view_path}/index.html.erb"
85
+ template ui_template('views/models/show.html.erb'), "app/views/#{model_view_path}/show.html.erb"
86
+ template ui_template('views/models/_model.html.erb'),
87
+ "app/views/#{model_view_path}/_#{model_model_name.demodulize.underscore}.html.erb"
88
+ end
89
+
90
+ def create_controllers
91
+ # For namespaced models, use the proper Rails convention path
92
+ chat_controller_path = chat_model_name.underscore.pluralize
93
+ message_controller_path = message_model_name.underscore.pluralize
94
+ model_controller_path = model_model_name.underscore.pluralize
95
+
96
+ template 'controllers/chats_controller.rb', "app/controllers/#{chat_controller_path}_controller.rb"
97
+ template 'controllers/messages_controller.rb', "app/controllers/#{message_controller_path}_controller.rb"
98
+ template 'controllers/models_controller.rb', "app/controllers/#{model_controller_path}_controller.rb"
99
+ end
100
+
101
+ def create_jobs
102
+ template 'jobs/chat_response_job.rb', "app/jobs/#{variable_name_for(chat_model_name)}_response_job.rb"
103
+ end
104
+
105
+ def create_helpers
106
+ template 'helpers/messages_helper.rb', "app/helpers/#{message_model_name.underscore.pluralize}_helper.rb"
107
+ end
108
+
109
+ def add_available_chat_models_to_application_controller
110
+ path = 'app/controllers/application_controller.rb'
111
+ return unless File.exist?(path)
112
+
113
+ application_controller = File.read(path)
114
+ return if application_controller.include?('def available_chat_models')
115
+
116
+ inject_into_file path, <<-RUBY, before: /^end\s*\z/
117
+ private
118
+
119
+ def available_chat_models
120
+ LexLLM.models.chat_models.all
121
+ .sort_by { |model| [ model.provider.to_s, model.name.to_s ] }
122
+ end
123
+ RUBY
124
+ end
125
+
126
+ def add_routes
127
+ # For namespaced models, use Rails convention with namespace blocks
128
+ if chat_model_name.include?('::')
129
+ namespace = chat_model_name.deconstantize.underscore
130
+ chat_resource = chat_model_name.demodulize.underscore.pluralize
131
+ message_resource = message_model_name.demodulize.underscore.pluralize
132
+ model_resource = model_model_name.demodulize.underscore.pluralize
133
+
134
+ routes_content = <<~ROUTES.strip
135
+ namespace :#{namespace} do
136
+ resources :#{model_resource}, only: [ :index, :show ] do
137
+ collection do
138
+ post :refresh
139
+ end
140
+ end
141
+ resources :#{chat_resource} do
142
+ resources :#{message_resource}, only: [ :create ]
143
+ end
144
+ end
145
+ ROUTES
146
+ route routes_content
147
+ else
148
+ model_routes = <<~ROUTES.strip
149
+ resources :#{model_table_name}, only: [ :index, :show ] do
150
+ collection do
151
+ post :refresh
152
+ end
153
+ end
154
+ ROUTES
155
+ route model_routes
156
+ chat_routes = <<~ROUTES.strip
157
+ resources :#{chat_table_name} do
158
+ resources :#{message_table_name}, only: [ :create ]
159
+ end
160
+ ROUTES
161
+ route chat_routes
162
+ end
163
+ end
164
+
165
+ def add_broadcasting_to_message_model
166
+ msg_var = variable_name_for(message_model_name)
167
+ chat_var = variable_name_for(chat_model_name)
168
+ msg_path = message_model_name.underscore
169
+
170
+ # For namespaced models, we need the association name which might be different
171
+ # e.g., for LLM::Message, the chat association might be :llm_chat
172
+ chat_association = chat_table_name.singularize
173
+
174
+ broadcasting_callbacks = <<-RUBY
175
+
176
+ broadcasts_to ->(#{msg_var}) { "#{chat_var}_\#{#{msg_var}.#{chat_association}_id}" }, inserts_by: :append
177
+
178
+ def broadcast_append_chunk(content)
179
+ broadcast_append_to "#{chat_var}_\#{#{chat_association}_id}",
180
+ target: "#{msg_var}_\#{id}_content",
181
+ content: ERB::Util.html_escape(content.to_s)
182
+ end
183
+ RUBY
184
+
185
+ inject_into_file "app/models/#{msg_path}.rb", before: "end\n" do
186
+ broadcasting_callbacks
187
+ end
188
+ rescue Errno::ENOENT
189
+ say "#{message_model_name} model not found. Add broadcasting code to your model.", :yellow
190
+ say broadcasting_callbacks, :yellow
191
+ end
192
+
193
+ def display_post_install_message
194
+ return unless behavior == :invoke
195
+
196
+ # Show the correct URL based on whether models are namespaced
197
+ url_path = if chat_model_name.include?('::')
198
+ chat_model_name.underscore.pluralize
199
+ else
200
+ chat_table_name
201
+ end
202
+
203
+ say "\n ✅ Chat UI installed!", :green
204
+ say " UI template: #{ui_variant}", :cyan
205
+ say "\n Start your server and visit http://localhost:3000/#{url_path}", :cyan
206
+ say "\n"
207
+ end
208
+
209
+ private
210
+
211
+ def ui_variant
212
+ @ui_variant ||= case options[:ui]
213
+ when 'tailwind'
214
+ :tailwind
215
+ when 'auto'
216
+ tailwind_available? ? :tailwind : :scaffold
217
+ else
218
+ :scaffold
219
+ end
220
+ end
221
+
222
+ def ui_template(template_path)
223
+ return template_path unless ui_variant == :tailwind
224
+
225
+ # Keep Tailwind templates as a separate set so we can mirror Rails/Tailwind
226
+ # scaffold conventions without complicating scaffold templates.
227
+ tailwind_template = "tailwind/#{template_path}"
228
+ File.exist?(File.join(self.class.source_root, "#{tailwind_template}.tt")) ? tailwind_template : template_path
229
+ end
230
+
231
+ def message_helper_module_name
232
+ if message_model_name.include?('::')
233
+ "#{message_model_name.deconstantize}::#{message_model_name.demodulize.pluralize}Helper"
234
+ else
235
+ "#{message_model_name.pluralize}Helper"
236
+ end
237
+ end
238
+
239
+ def tailwind_available?
240
+ Rails.root.join('app/assets/tailwind/application.css').exist? ||
241
+ Rails.root.join('config/tailwind.config.js').exist? ||
242
+ gem_in_bundle?('tailwindcss-rails') ||
243
+ gem_in_bundle?('cssbundling-rails')
244
+ end
245
+
246
+ def gem_in_bundle?(gem_name)
247
+ gemfile_path = Rails.root.join('Gemfile')
248
+ lockfile_path = Rails.root.join('Gemfile.lock')
249
+
250
+ [gemfile_path, lockfile_path].any? do |path|
251
+ path.exist? && path.read.include?(gem_name)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,38 @@
1
+ class <%= chat_controller_class_name %> < ApplicationController
2
+ before_action :set_<%= chat_variable_name %>, only: [ :show, :destroy ]
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
+ @chat_models = available_chat_models
12
+ end
13
+
14
+ def create
15
+ prompt = params.dig(:<%= chat_variable_name %>, :prompt)
16
+ if prompt.present?
17
+ @<%= chat_variable_name %> = <%= chat_model_name %>.create!(model: params.dig(:<%= chat_variable_name %>, :model).presence)
18
+ <%= chat_job_class_name %>.perform_later(@<%= chat_variable_name %>.id, prompt)
19
+
20
+ redirect_to @<%= chat_variable_name %>, notice: "<%= chat_model_name.humanize %> was successfully created."
21
+ end
22
+ end
23
+
24
+ def show
25
+ @<%= message_variable_name %> = @<%= chat_variable_name %>.<%= message_table_name %>.build
26
+ end
27
+
28
+ def destroy
29
+ @<%= chat_variable_name %>.destroy!
30
+ redirect_to <%= chat_table_name %>_path, notice: "<%= chat_model_name.humanize %> was successfully destroyed.", status: :see_other
31
+ end
32
+
33
+ private
34
+
35
+ def set_<%= chat_variable_name %>
36
+ @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:id])
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ class <%= message_controller_class_name %> < ApplicationController
2
+ before_action :set_<%= chat_variable_name %>
3
+
4
+ def create
5
+ content = params.dig(:<%= message_variable_name %>, :content)
6
+ if content.present?
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
+ end
15
+
16
+ private
17
+
18
+ def set_<%= chat_variable_name %>
19
+ @<%= chat_variable_name %> = <%= chat_model_name %>.find(params[:<%= chat_model_name.include?('::') ? chat_model_name.demodulize.underscore : chat_variable_name %>_id])
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ class <%= model_controller_class_name %> < ApplicationController
2
+ def index
3
+ @<%= model_table_name %> = available_chat_models
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,25 @@
1
+ module <%= message_helper_module_name %>
2
+ def default_model_display_name
3
+ "Default: #{LexLLM.models.find(LexLLM.config.default_model).label}"
4
+ end
5
+
6
+ def tool_result_partial(message)
7
+ name = message.respond_to?(:parent_tool_call) ? message.parent_tool_call&.name.to_s : ""
8
+ partial_for(prefix: "<%= message_model_name.underscore.pluralize %>/tool_results", name: name)
9
+ end
10
+
11
+ def tool_call_partial(tool_call)
12
+ partial_for(prefix: "<%= message_model_name.underscore.pluralize %>/tool_calls", name: tool_call.name.to_s)
13
+ end
14
+
15
+ private
16
+
17
+ def partial_for(prefix:, name:)
18
+ normalized = name.to_s.underscore.tr("-", "_")
19
+ if normalized.present? && lookup_context.exists?(normalized, [ prefix ], true)
20
+ "#{prefix}/#{normalized}"
21
+ else
22
+ "#{prefix}/default"
23
+ end
24
+ end
25
+ end
@@ -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.empty?
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 %> %>" class="w-full sm:w-auto my-5 space-y-3">
2
+ <div>
3
+ <strong class="block font-medium mb-1">Model:</strong>
4
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= model_table_name.singularize %>&.label || default_model_display_name %>
5
+ </div>
6
+
7
+ <div>
8
+ <strong class="block font-medium mb-1">Messages:</strong>
9
+ <%%= <%= chat_model_name.demodulize.underscore %>.<%= message_table_name %>.count %>
10
+ </div>
11
+
12
+ <div>
13
+ <strong class="block font-medium mb-1">Created:</strong>
14
+ <%%= <%= chat_model_name.demodulize.underscore %>.created_at.strftime("%B %d, %Y at %I:%M %p") %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,31 @@
1
+ <%%= form_with(model: <%= chat_variable_name %>, url: <%= chat_table_name %>_path, class: "contents") do |form| %>
2
+ <%% if <%= chat_variable_name %>.errors.any? %>
3
+ <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3">
4
+ <h2><%%= pluralize(<%= chat_variable_name %>.errors.count, "error") %> prohibited this <%= chat_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul class="list-disc ml-6">
7
+ <%% <%= chat_variable_name %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div class="my-5">
15
+ <%%= form.label :model, "Select AI model:" %>
16
+ <%%= form.select :model,
17
+ options_for_select(@chat_models.map { |model| [model.label, model.id] }.unshift([default_model_display_name, nil]), @selected_model),
18
+ {},
19
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= chat_variable_name %>.errors[:model].none?, "border-red-400 focus:outline-red-600": <%= chat_variable_name %>.errors[:model].any?}] %>
20
+ </div>
21
+
22
+ <div class="my-5">
23
+ <%%= form.label :prompt, "Prompt" %>
24
+ <%%= form.text_area :prompt, rows: 4, placeholder: "What would you like to discuss?", autofocus: true,
25
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= chat_variable_name %>.errors[:prompt].none?, "border-red-400 focus:outline-red-600": <%= chat_variable_name %>.errors[:prompt].any?}] %>
26
+ </div>
27
+
28
+ <div class="inline">
29
+ <%%= form.submit "Start new <%= chat_table_name.singularize.humanize.downcase %>", class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
30
+ </div>
31
+ <%% end %>
@@ -0,0 +1,31 @@
1
+ <%% content_for :title, "<%= chat_model_name.pluralize %>" %>
2
+
3
+ <div class="w-full">
4
+ <%% if notice.present? %>
5
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
6
+ <%% end %>
7
+
8
+ <div class="flex justify-between items-center">
9
+ <h1 class="font-bold text-4xl"><%= chat_model_name.pluralize %></h1>
10
+ <div class="flex items-center gap-2">
11
+ <%%= link_to "<%= model_model_name.pluralize %>", <%= model_table_name %>_path, class: "rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 text-gray-900 block font-medium" %>
12
+ <%%= link_to "New <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div id="<%= chat_table_name %>" class="min-w-full divide-y divide-gray-200 space-y-5">
17
+ <%% if @<%= chat_table_name %>.any? %>
18
+ <%% @<%= chat_table_name %>.each do |<%= chat_variable_name %>| %>
19
+ <div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
20
+ <%%= render <%= chat_variable_name %> %>
21
+ <div class="w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2">
22
+ <%%= link_to "Show", <%= chat_variable_name %>, class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
23
+ <%%= button_to "Destroy", <%= chat_variable_name %>, method: :delete, class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-red-600 hover:bg-red-500 text-white inline-block font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
24
+ </div>
25
+ </div>
26
+ <%% end %>
27
+ <%% else %>
28
+ <p class="text-center my-10">No <%= chat_table_name.humanize.downcase %> found.</p>
29
+ <%% end %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,9 @@
1
+ <%% content_for :title, "New <%= chat_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <div class="md:w-2/3 w-full">
4
+ <h1 class="font-bold text-4xl">New <%= chat_table_name.singularize.humanize.downcase %></h1>
5
+
6
+ <%%= render "form", <%= chat_variable_name %>: @<%= chat_variable_name %> %>
7
+
8
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
9
+ </div>
@@ -0,0 +1,27 @@
1
+ <%% content_for :title, "Showing <%= chat_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <%%= turbo_stream_from "<%= chat_variable_name %>_#{@<%= chat_variable_name %>.id}" %>
4
+
5
+ <%# Keep layout conventions aligned with tailwindcss-rails scaffold (top-left, md:w-2/3). -%>
6
+ <div class="md:w-2/3 w-full">
7
+ <%% if notice.present? %>
8
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
9
+ <%% end %>
10
+
11
+ <h1 class="font-bold text-4xl">Showing <%= chat_table_name.singularize.humanize.downcase %> #<%%= @<%= chat_variable_name %>.id %></h1>
12
+
13
+ <div class="my-5">
14
+ <strong class="block font-medium mb-1">Model:</strong>
15
+ <%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>&.label || default_model_display_name %>
16
+ </div>
17
+
18
+ <div id="<%= message_table_name %>" class="min-w-full divide-y divide-gray-200 space-y-5 my-5">
19
+ <%% @<%= chat_variable_name %>.<%= message_table_name %>.where.not(id: nil).each do |<%= message_variable_name %>| %>
20
+ <%%= render <%= message_variable_name %> %>
21
+ <%% end %>
22
+ </div>
23
+
24
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= message_variable_name %> %>
25
+
26
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
27
+ </div>
@@ -0,0 +1,14 @@
1
+ <%% assistant ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-green-50">
3
+ <div>
4
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700">
5
+ Assistant
6
+ </span>
7
+ </div>
8
+
9
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>_content" class="whitespace-pre-wrap"><%%= assistant.content %></div>
10
+
11
+ <div>
12
+ <span class="text-sm text-gray-600"><%%= assistant.created_at&.strftime("%I:%M %p") %></span>
13
+ </div>
14
+ </div>
@@ -0,0 +1,13 @@
1
+ <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-red-50 border border-red-200">
2
+ <div>
3
+ <span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700">
4
+ <%%= title.presence || "Error" %>
5
+ </span>
6
+ </div>
7
+
8
+ <pre class="whitespace-pre-wrap text-red-900 text-sm overflow-x-auto"><%%= error_message %></pre>
9
+
10
+ <div>
11
+ <span class="text-sm text-red-700"><%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %></span>
12
+ </div>
13
+ </div>
@@ -0,0 +1,23 @@
1
+ <%%= form_with(model: <%= message_variable_name %>, url: <%= chat_model_name.include?('::') ? "#{chat_model_name.split('::').first.underscore}_#{chat_model_name.demodulize.underscore}_#{message_model_name.demodulize.underscore.pluralize}_path(@#{chat_variable_name})" : "[@#{chat_variable_name}, #{message_variable_name}]" %>, id: "new_<%= message_variable_name %>", class: "contents") do |form| %>
2
+ <%% if <%= message_variable_name %>.errors.any? %>
3
+ <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3">
4
+ <h2><%%= pluralize(<%= message_variable_name %>.errors.count, "error") %> prohibited this <%= message_table_name.singularize.humanize.downcase %> from being saved:</h2>
5
+
6
+ <ul class="list-disc ml-6">
7
+ <%% <%= message_variable_name %>.errors.each do |error| %>
8
+ <li><%%= error.full_message %></li>
9
+ <%% end %>
10
+ </ul>
11
+ </div>
12
+ <%% end %>
13
+
14
+ <div class="my-5">
15
+ <%%= form.label :content, "Message" %>
16
+ <%%= form.text_area :content, rows: 4, placeholder: "Message...", autofocus: true,
17
+ class: ["block shadow-sm rounded-md border px-3 py-2 mt-2 w-full", {"border-gray-400 focus:outline-blue-600": <%= message_variable_name %>.errors[:content].none?, "border-red-400 focus:outline-red-600": <%= message_variable_name %>.errors[:content].any?}] %>
18
+ </div>
19
+
20
+ <div class="inline">
21
+ <%%= form.submit "Send <%= message_table_name.singularize.humanize.downcase %>", class: "w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
22
+ </div>
23
+ <%% end %>
@@ -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>