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,40 @@
1
+ <%% content_for :title, "<%= model_model_name.pluralize %>" %>
2
+
3
+ <div class="w-full">
4
+ <%% if notice.present? %>
5
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
6
+ <%% end %>
7
+
8
+ <div class="flex justify-between items-center">
9
+ <h1 class="font-bold text-4xl"><%= model_model_name.pluralize %></h1>
10
+ <div class="flex items-center gap-2">
11
+ <%%= link_to "<%= chat_model_name.pluralize %>", <%= chat_table_name %>_path, class: "rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 text-gray-900 block font-medium" %>
12
+ <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium cursor-pointer" %>
13
+ </div>
14
+ </div>
15
+
16
+ <div id="<%= model_table_name %>" class="mt-5 overflow-x-auto">
17
+ <%% if @<%= model_variable_name.pluralize %>.any? %>
18
+ <table class="min-w-full divide-y divide-gray-300 border border-gray-300">
19
+ <thead class="bg-gray-50">
20
+ <tr>
21
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Provider</th>
22
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Model</th>
23
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">Context Window</th>
24
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900">$/1M tokens (In/Out)</th>
25
+ <th class="px-3 py-2 text-left text-sm font-semibold text-gray-900"></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="divide-y divide-gray-200 bg-white">
29
+ <%% @<%= model_variable_name.pluralize %>.each do |model_info| %>
30
+ <%%= render "<%= model_model_name.underscore.pluralize %>/<%= model_model_name.demodulize.underscore %>",
31
+ <%= model_model_name.demodulize.underscore %>: model_info %>
32
+ <%% end %>
33
+ </tbody>
34
+ </table>
35
+ <%% else %>
36
+ <p class="text-center my-10">No chat models found.</p>
37
+ <%% end %>
38
+ </div>
39
+
40
+ </div>
@@ -0,0 +1,27 @@
1
+ <%% content_for :title, "Showing <%= model_table_name.singularize.humanize.downcase %>" %>
2
+
3
+ <div class="md:w-2/3 w-full">
4
+ <%% if notice.present? %>
5
+ <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%%= notice %></p>
6
+ <%% end %>
7
+
8
+ <h1 class="font-bold text-4xl">Showing <%= model_table_name.singularize.humanize.downcase %></h1>
9
+
10
+ <div class="my-5 space-y-3">
11
+ <p><strong class="block font-medium mb-1">Provider:</strong> <%%= @<%= model_variable_name %>.provider_class&.name || @<%= model_variable_name %>.provider %></p>
12
+ <p><strong class="block font-medium mb-1">Model:</strong> <%%= @<%= model_variable_name %>.name %></p>
13
+ <p><strong class="block font-medium mb-1">ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
14
+ <p><strong class="block font-medium mb-1">Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) if @<%= model_variable_name %>.context_window %></p>
15
+ <p><strong class="block font-medium mb-1">Max Output Tokens:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) if @<%= model_variable_name %>.max_output_tokens %></p>
16
+ </div>
17
+
18
+ <%% if @<%= model_variable_name %>.capabilities.any? %>
19
+ <div class="my-5">
20
+ <strong class="block font-medium mb-1">Capabilities:</strong>
21
+ <%%= @<%= model_variable_name %>.capabilities.join(", ") %>
22
+ </div>
23
+ <%% end %>
24
+
25
+ <%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %> with this model", new_<%= chat_variable_name %>_path(model: @<%= model_variable_name %>.model_id), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
26
+ <%%= link_to "All <%= model_table_name.humanize.downcase %>", <%= model_table_name %>_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
27
+ </div>
@@ -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 %>&.label || default_model_display_name %>
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(@chat_models.map { |model| [model.label, model.id] }.unshift([default_model_display_name, 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,28 @@
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
4
+
5
+ <%% content_for :title, "<%= chat_model_name.pluralize %>" %>
6
+
7
+ <h1><%= chat_model_name.pluralize %></h1>
8
+ <p>
9
+ <%%= link_to "<%= model_model_name.pluralize %>", <%= model_table_name %>_path %>
10
+ </p>
11
+
12
+ <div id="<%= chat_table_name %>">
13
+ <%% if @<%= chat_table_name %>.any? %>
14
+ <%% @<%= chat_table_name %>.each do |<%= chat_variable_name %>| %>
15
+ <%%= render <%= chat_variable_name %> %>
16
+ <p>
17
+ <%%= link_to "Show this <%= chat_table_name.singularize.humanize.downcase %>", <%= chat_variable_name %> %>
18
+ </p>
19
+ <p>
20
+ <%%= link_to "Destroy this <%= chat_table_name.singularize.humanize.downcase %>", <%= chat_variable_name %>, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
21
+ </p>
22
+ <%% end %>
23
+ <%% else %>
24
+ <p>No <%= chat_table_name.humanize.downcase %> found.</p>
25
+ <%% end %>
26
+ </div>
27
+
28
+ <%%= 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,25 @@
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
4
+
5
+ <%%= turbo_stream_from "<%= chat_variable_name %>_#{@<%= chat_variable_name %>.id}" %>
6
+
7
+ <%% content_for :title, "<%= chat_model_name %>" %>
8
+
9
+ <h1><%= chat_model_name %> <%%= @<%= chat_variable_name %>.id %></h1>
10
+
11
+ <p>Using <strong><%%= @<%= chat_variable_name %>.<%= model_table_name.singularize %>&.label || default_model_display_name %></strong></p>
12
+
13
+ <div id="<%= message_table_name %>">
14
+ <%% @<%= chat_variable_name %>.<%= message_table_name %>.where.not(id: nil).each do |<%= message_variable_name %>| %>
15
+ <%%= render <%= message_variable_name %> %>
16
+ <%% end %>
17
+ </div>
18
+
19
+ <div style="margin-top: 30px;">
20
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= message_variable_name %> %>
21
+ </div>
22
+
23
+ <div style="margin-top: 20px;">
24
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
25
+ </div>
@@ -0,0 +1,9 @@
1
+ <%% assistant ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #28a745;">
4
+ <div style="font-weight: bold; margin-bottom: 5px;">Assistant</div>
5
+ <div id="<%= message_variable_name %>_<%%= assistant.id %>_content" style="white-space: pre-wrap;"><%%= assistant.content %></div>
6
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
7
+ <%%= assistant.created_at&.strftime("%I:%M %p") %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,8 @@
1
+ <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>" class="<%= message_variable_name %>"
2
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #dc2626; background: #fef2f2;">
3
+ <div style="font-weight: bold; margin-bottom: 5px; color: #b91c1c;"><%%= title.presence || "Error" %></div>
4
+ <pre style="white-space: pre-wrap; margin: 0; color: #7f1d1d;"><%%= error_message %></pre>
5
+ <div style="font-size: 0.85em; color: #7f1d1d; margin-top: 5px;">
6
+ <%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %>
7
+ </div>
8
+ </div>
@@ -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,6 @@
1
+ <%% system ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= system.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
4
+ <div style="font-weight: bold; margin-bottom: 5px; color: #374151;">System</div>
5
+ <div style="white-space: pre-wrap; color: #4b5563;"><%%= system.content %></div>
6
+ </div>
@@ -0,0 +1,2 @@
1
+ <%% tool ||= local_assigns[:message] %>
2
+ <%%= render tool_result_partial(tool), tool: tool %>
@@ -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,9 @@
1
+ <%% user ||= local_assigns[:message] %>
2
+ <div id="<%= message_variable_name %>_<%%= user.id %>" class="<%= message_variable_name %>"
3
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #007bff;">
4
+ <div style="font-weight: bold; margin-bottom: 5px;">User</div>
5
+ <div id="<%= message_variable_name %>_<%%= user.id %>_content" style="white-space: pre-wrap;"><%%= user.content %></div>
6
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
7
+ <%%= user.created_at&.strftime("%I:%M %p") %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,7 @@
1
+ <%% if defined?(@<%= chat_variable_name %>) && @<%= chat_variable_name %>.present? %>
2
+ <%%= turbo_stream.replace "new_<%= message_variable_name %>" do %>
3
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form",
4
+ <%= chat_variable_name %>: @<%= chat_variable_name %>,
5
+ <%= message_variable_name %>: @<%= chat_variable_name %>.<%= message_table_name %>.build %>
6
+ <%% end %>
7
+ <%% end %>
@@ -0,0 +1,8 @@
1
+ <div id="<%= message_variable_name %>_<%%= tool_calls.id %>" class="<%= message_variable_name %>"
2
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
3
+ <div style="font-weight: bold; margin-bottom: 5px;">Tool Call</div>
4
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
5
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
6
+ <%%= tool_calls.created_at&.strftime("%I:%M %p") %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,16 @@
1
+ <%% error_message = tool.tool_error_message %>
2
+ <%% if error_message.present? %>
3
+ <%%= render "<%= message_model_name.underscore.pluralize %>/error",
4
+ <%= message_model_name.demodulize.underscore %>: tool,
5
+ title: "Tool Result Error",
6
+ error_message: error_message %>
7
+ <%% else %>
8
+ <div id="<%= message_variable_name %>_<%%= tool.id %>" class="<%= message_variable_name %>"
9
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
10
+ <div style="font-weight: bold; margin-bottom: 5px;">Tool</div>
11
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
12
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
13
+ <%%= tool.created_at&.strftime("%I:%M %p") %>
14
+ </div>
15
+ </div>
16
+ <%% end %>
@@ -0,0 +1,15 @@
1
+ <%% model_id = <%= model_model_name.demodulize.underscore %>.respond_to?(:model_id) ? <%= model_model_name.demodulize.underscore %>.model_id : <%= model_model_name.demodulize.underscore %>.id %>
2
+ <%% row_id = [<%= model_model_name.demodulize.underscore %>.provider, model_id].join("_").parameterize(separator: "_") %>
3
+ <tr id="model_<%%= row_id %>">
4
+ <td><%%= <%= model_model_name.demodulize.underscore %>.provider_class&.name || <%= model_model_name.demodulize.underscore %>.provider %></td>
5
+ <td><%%= <%= model_model_name.demodulize.underscore %>.respond_to?(:display_name) ? <%= model_model_name.demodulize.underscore %>.display_name : <%= model_model_name.demodulize.underscore %>.name %></td>
6
+ <td><%%= number_with_delimiter(<%= model_model_name.demodulize.underscore %>.context_window) if <%= model_model_name.demodulize.underscore %>.context_window %></td>
7
+ <td>
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><%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path(model: model_id) %></td>
15
+ </tr>
@@ -0,0 +1,38 @@
1
+ <%% if notice.present? %>
2
+ <p style="color: green"><%%= notice %></p>
3
+ <%% end %>
4
+
5
+ <%% content_for :title, "<%= model_model_name.pluralize %>" %>
6
+
7
+ <h1><%= model_model_name.pluralize %></h1>
8
+ <p>
9
+ <%%= link_to "<%= chat_model_name.pluralize %>", <%= chat_table_name %>_path %>
10
+ </p>
11
+
12
+ <p>
13
+ <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post %>
14
+ </p>
15
+
16
+ <div id="<%= model_table_name %>">
17
+ <%% if @<%= model_variable_name.pluralize %>.any? %>
18
+ <table>
19
+ <thead>
20
+ <tr>
21
+ <th>Provider</th>
22
+ <th>Model</th>
23
+ <th>Context Window</th>
24
+ <th>$/1M tokens (In/Out)</th>
25
+ <th></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <%% @<%= model_variable_name.pluralize %>.each do |model_info| %>
30
+ <%%= render "<%= model_model_name.underscore.pluralize %>/<%= model_model_name.demodulize.underscore %>",
31
+ <%= model_model_name.demodulize.underscore %>: model_info %>
32
+ <%% end %>
33
+ </tbody>
34
+ </table>
35
+ <%% else %>
36
+ <p>No chat models found.</p>
37
+ <%% end %>
38
+ </div>
@@ -0,0 +1,17 @@
1
+ <%% content_for :title, @<%= model_variable_name %>.name %>
2
+
3
+ <h1><%%= @<%= model_variable_name %>.name %></h1>
4
+
5
+ <p><strong>ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
6
+ <p><strong>Provider:</strong> <%%= @<%= model_variable_name %>.provider_class&.name || @<%= model_variable_name %>.provider %></p>
7
+ <p><strong>Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) %> tokens</p>
8
+ <p><strong>Max Output:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) %> tokens</p>
9
+
10
+ <%% if @<%= model_variable_name %>.capabilities.any? %>
11
+ <p><strong>Capabilities:</strong> <%%= @<%= model_variable_name %>.capabilities.join(", ") %></p>
12
+ <%% end %>
13
+
14
+ <p>
15
+ <%%= link_to "Start chat with this model", new_<%= chat_variable_name %>_path(model: @<%= model_variable_name %>.model_id) %> |
16
+ <%%= link_to "All models", <%= model_table_name %>_path %>
17
+ </p>
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ module Generators
5
+ # Shared helpers for LexLLM generators
6
+ module GeneratorHelpers
7
+ def parse_model_mappings
8
+ @model_names = {
9
+ chat: 'Chat',
10
+ message: 'Message',
11
+ tool_call: 'ToolCall',
12
+ model: 'Model'
13
+ }
14
+
15
+ model_mappings.each do |mapping|
16
+ if mapping.include?(':')
17
+ key, value = mapping.split(':', 2)
18
+ @model_names[key.to_sym] = value.classify
19
+ end
20
+ end
21
+
22
+ @model_names
23
+ end
24
+
25
+ %i[chat message tool_call model].each do |type|
26
+ define_method("#{type}_model_name") do
27
+ @model_names ||= parse_model_mappings
28
+ @model_names[type]
29
+ end
30
+
31
+ define_method("#{type}_table_name") do
32
+ table_name_for(send("#{type}_model_name"))
33
+ end
34
+
35
+ define_method("#{type}_variable_name") do
36
+ variable_name_for(send("#{type}_model_name"))
37
+ end
38
+
39
+ define_method("#{type}_controller_class_name") do
40
+ controller_class_name_for(send("#{type}_model_name"))
41
+ end
42
+
43
+ define_method("#{type}_job_class_name") do
44
+ "#{variable_name_for(send("#{type}_model_name")).camelize}ResponseJob"
45
+ end
46
+
47
+ define_method("#{type}_partial") do
48
+ partial_path_for(send("#{type}_model_name"))
49
+ end
50
+ end
51
+
52
+ def acts_as_chat_declaration
53
+ params = []
54
+
55
+ add_association_params(params, :messages, message_table_name, message_model_name,
56
+ owner_table: chat_table_name, owner_model_name: chat_model_name, plural: true)
57
+ add_association_params(params, :model, model_table_name, model_model_name,
58
+ owner_table: chat_table_name, owner_model_name: chat_model_name)
59
+
60
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
61
+ end
62
+
63
+ def acts_as_message_declaration
64
+ params = []
65
+
66
+ add_association_params(params, :chat, chat_table_name, chat_model_name,
67
+ owner_table: message_table_name, owner_model_name: message_model_name)
68
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
69
+ owner_table: message_table_name, owner_model_name: message_model_name, plural: true)
70
+ add_association_params(params, :model, model_table_name, model_model_name,
71
+ owner_table: message_table_name, owner_model_name: message_model_name)
72
+
73
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
74
+ end
75
+
76
+ def acts_as_model_declaration
77
+ params = []
78
+
79
+ add_association_params(params, :chats, chat_table_name, chat_model_name,
80
+ owner_table: model_table_name, owner_model_name: model_model_name, plural: true)
81
+
82
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
83
+ end
84
+
85
+ def acts_as_tool_call_declaration
86
+ params = []
87
+
88
+ add_association_params(params, :message, message_table_name, message_model_name,
89
+ owner_table: tool_call_table_name, owner_model_name: tool_call_model_name)
90
+
91
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
+ end
93
+
94
+ def create_namespace_modules
95
+ namespaces = []
96
+
97
+ [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
98
+ if model_name.include?('::')
99
+ namespace = model_name.split('::').first
100
+ namespaces << namespace unless namespaces.include?(namespace)
101
+ end
102
+ end
103
+
104
+ namespaces.each do |namespace|
105
+ module_path = "app/models/#{namespace.underscore}.rb"
106
+ next if File.exist?(Rails.root.join(module_path))
107
+
108
+ create_file module_path do
109
+ <<~RUBY
110
+ module #{namespace}
111
+ def self.table_name_prefix
112
+ "#{namespace.underscore}_"
113
+ end
114
+ end
115
+ RUBY
116
+ end
117
+ end
118
+ end
119
+
120
+ def migration_version
121
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
122
+ end
123
+
124
+ def create_migration_class_name(table_name)
125
+ "create_#{table_name}".camelize
126
+ end
127
+
128
+ def postgresql?
129
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
130
+ rescue StandardError
131
+ false
132
+ end
133
+
134
+ def mysql?
135
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
136
+ rescue StandardError
137
+ false
138
+ end
139
+
140
+ def table_exists?(table_name)
141
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
142
+ rescue StandardError
143
+ false
144
+ end
145
+
146
+ private
147
+
148
+ # rubocop:disable Metrics/ParameterLists
149
+ def add_association_params(params, default_assoc, table_name, model_name,
150
+ owner_table:, owner_model_name:, plural: false)
151
+ assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
152
+ collection_association = collection_association?(default_assoc, plural)
153
+ foreign_key = inferred_foreign_key(table_name, owner_table, collection_association)
154
+ default_foreign_key = default_inferred_foreign_key(assoc, owner_model_name, collection_association)
155
+
156
+ params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
157
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
158
+ params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
159
+ end
160
+ # rubocop:enable Metrics/ParameterLists
161
+
162
+ def collection_association?(default_assoc, plural)
163
+ plural || default_assoc.to_s.pluralize == default_assoc.to_s
164
+ end
165
+
166
+ def inferred_foreign_key(table_name, owner_table, collection_association)
167
+ return "#{table_name.singularize}_id" unless collection_association
168
+
169
+ "#{owner_table.singularize}_id"
170
+ end
171
+
172
+ # Rails default inference:
173
+ # belongs_to :assoc -> assoc_id
174
+ # has_many/has_one -> owner demodulized model name + _id
175
+ def default_inferred_foreign_key(association_name, owner_model_name, collection_association)
176
+ return "#{association_name}_id" unless collection_association
177
+
178
+ "#{owner_model_name.demodulize.underscore}_id"
179
+ end
180
+
181
+ # Convert namespaced model names to proper table names
182
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
183
+ def table_name_for(model_name)
184
+ model_name.underscore.pluralize.tr('/', '_')
185
+ end
186
+
187
+ # Convert model name to instance variable name
188
+ # e.g., "LLM::Chat" -> "llm_chat" (not "llm/chat")
189
+ def variable_name_for(model_name)
190
+ model_name.underscore.tr('/', '_')
191
+ end
192
+
193
+ # Convert model name to controller class name
194
+ # For namespaced models, use Rails convention: "Llm::Chat" -> "Llm::ChatsController"
195
+ # For regular models: "Chat" -> "ChatsController"
196
+ def controller_class_name_for(model_name)
197
+ if model_name.include?('::')
198
+ parts = model_name.split('::')
199
+ namespace = parts[0..-2].join('::')
200
+ resource = parts.last.pluralize
201
+ "#{namespace}::#{resource}Controller"
202
+ else
203
+ "#{model_name.pluralize}Controller"
204
+ end
205
+ end
206
+
207
+ # Convert model name to partial path
208
+ # e.g., "LLM::Message" -> "llm/message" (not "llm_message")
209
+ def partial_path_for(model_name)
210
+ "#{model_name.underscore.pluralize}/#{model_name.demodulize.underscore}"
211
+ end
212
+ end
213
+ end
214
+ end