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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +7 -0
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +42 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +50 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/lex-llm.gemspec +43 -0
- data/lib/generators/lex_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/lex_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +256 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +38 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +28 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +25 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +15 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +38 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +17 -0
- data/lib/generators/lex_llm/generator_helpers.rb +214 -0
- data/lib/generators/lex_llm/install/install_generator.rb +109 -0
- data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
- data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +7 -0
- data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +19 -0
- data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +39 -0
- data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +21 -0
- data/lib/generators/lex_llm/install/templates/initializer.rb.tt +20 -0
- data/lib/generators/lex_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/lex_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/lex_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/lex_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/lex_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/lex_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +122 -0
- data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
- data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
- data/lib/legion/extensions/llm/provider_settings.rb +49 -0
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +70 -0
- data/lib/legion/extensions/llm.rb +50 -0
- data/lib/lex_llm/active_record/acts_as.rb +180 -0
- data/lib/lex_llm/active_record/acts_as_legacy.rb +503 -0
- data/lib/lex_llm/active_record/chat_methods.rb +468 -0
- data/lib/lex_llm/active_record/message_methods.rb +131 -0
- data/lib/lex_llm/active_record/model_methods.rb +76 -0
- data/lib/lex_llm/active_record/payload_helpers.rb +26 -0
- data/lib/lex_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/lex_llm/agent.rb +365 -0
- data/lib/lex_llm/aliases.json +436 -0
- data/lib/lex_llm/aliases.rb +38 -0
- data/lib/lex_llm/attachment.rb +223 -0
- data/lib/lex_llm/chat.rb +351 -0
- data/lib/lex_llm/chunk.rb +6 -0
- data/lib/lex_llm/configuration.rb +81 -0
- data/lib/lex_llm/connection.rb +130 -0
- data/lib/lex_llm/content.rb +77 -0
- data/lib/lex_llm/context.rb +29 -0
- data/lib/lex_llm/embedding.rb +29 -0
- data/lib/lex_llm/error.rb +112 -0
- data/lib/lex_llm/image.rb +105 -0
- data/lib/lex_llm/message.rb +107 -0
- data/lib/lex_llm/mime_type.rb +71 -0
- data/lib/lex_llm/model/info.rb +113 -0
- data/lib/lex_llm/model/modalities.rb +22 -0
- data/lib/lex_llm/model/pricing.rb +48 -0
- data/lib/lex_llm/model/pricing_category.rb +46 -0
- data/lib/lex_llm/model/pricing_tier.rb +33 -0
- data/lib/lex_llm/model.rb +7 -0
- data/lib/lex_llm/models.json +57241 -0
- data/lib/lex_llm/models.rb +506 -0
- data/lib/lex_llm/models_schema.json +168 -0
- data/lib/lex_llm/moderation.rb +56 -0
- data/lib/lex_llm/provider.rb +278 -0
- data/lib/lex_llm/railtie.rb +35 -0
- data/lib/lex_llm/routing/lane_key.rb +51 -0
- data/lib/lex_llm/routing/model_offering.rb +169 -0
- data/lib/lex_llm/routing.rb +7 -0
- data/lib/lex_llm/stream_accumulator.rb +203 -0
- data/lib/lex_llm/streaming.rb +175 -0
- data/lib/lex_llm/thinking.rb +49 -0
- data/lib/lex_llm/tokens.rb +47 -0
- data/lib/lex_llm/tool.rb +254 -0
- data/lib/lex_llm/tool_call.rb +25 -0
- data/lib/lex_llm/transcription.rb +35 -0
- data/lib/lex_llm/utils.rb +91 -0
- data/lib/lex_llm/version.rb +5 -0
- data/lib/lex_llm.rb +95 -0
- data/lib/tasks/lex_llm.rake +23 -0
- metadata +349 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
class MigrateToLexLlmModelReferences < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
model_class = <%= model_model_name %>
|
|
4
|
+
chat_class = <%= chat_model_name %>
|
|
5
|
+
message_class = <%= message_model_name %>
|
|
6
|
+
<% if @model_table_already_existed %>
|
|
7
|
+
# Load models from models.json if Model table already existed
|
|
8
|
+
say_with_time "Loading models from models.json" do
|
|
9
|
+
LexLLM.models.load_from_json!
|
|
10
|
+
model_class.save_to_database
|
|
11
|
+
"Loaded #{model_class.count} models"
|
|
12
|
+
end
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
# Then check for any models in existing data that aren't in models.json
|
|
16
|
+
say_with_time "Checking for additional models in existing data" do
|
|
17
|
+
collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
|
|
18
|
+
collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
|
|
19
|
+
model_class.count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Migrate foreign keys
|
|
23
|
+
migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
|
|
24
|
+
migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def down
|
|
28
|
+
# Remove foreign key references
|
|
29
|
+
if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
|
|
30
|
+
remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
|
|
34
|
+
remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Restore original model_id string columns
|
|
38
|
+
if column_exists?(:<%= message_table_name %>, :model_id_string)
|
|
39
|
+
rename_column :<%= message_table_name %>, :model_id_string, :model_id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if column_exists?(:<%= chat_table_name %>, :model_id_string)
|
|
43
|
+
rename_column :<%= chat_table_name %>, :model_id_string, :model_id
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collect_and_create_models(record_class, table_name, model_class)
|
|
50
|
+
return unless column_exists?(table_name, :model_id)
|
|
51
|
+
|
|
52
|
+
has_provider = column_exists?(table_name, :provider)
|
|
53
|
+
|
|
54
|
+
# Collect unique model/provider combinations using read_attribute to bypass overrides
|
|
55
|
+
models_set = Set.new
|
|
56
|
+
|
|
57
|
+
record_class.find_each do |record|
|
|
58
|
+
model_id = record.read_attribute(:model_id)
|
|
59
|
+
next if model_id.blank?
|
|
60
|
+
|
|
61
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
|
62
|
+
models_set.add([ model_id, provider ])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
models_set.each do |model_id, provider|
|
|
66
|
+
find_or_create_model(model_id, provider, model_class)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_or_create_model(model_id, provider, model_class)
|
|
71
|
+
return if model_id.blank?
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
model_info, _provider = LexLLM.models.resolve(model_id, provider: provider)
|
|
75
|
+
|
|
76
|
+
model_class.find_or_create_by!(
|
|
77
|
+
model_id: model_info.id,
|
|
78
|
+
provider: model_info.provider
|
|
79
|
+
) do |m|
|
|
80
|
+
m.name = model_info.name || model_info.id
|
|
81
|
+
m.family = model_info.family
|
|
82
|
+
m.model_created_at = model_info.created_at
|
|
83
|
+
m.context_window = model_info.context_window
|
|
84
|
+
m.max_output_tokens = model_info.max_output_tokens
|
|
85
|
+
m.knowledge_cutoff = model_info.knowledge_cutoff
|
|
86
|
+
m.modalities = model_info.modalities.to_h
|
|
87
|
+
m.capabilities = model_info.capabilities
|
|
88
|
+
m.pricing = model_info.pricing.to_h
|
|
89
|
+
m.metadata = model_info.metadata
|
|
90
|
+
end
|
|
91
|
+
rescue => e
|
|
92
|
+
# Skip models that can't be resolved - they'll need manual fixing
|
|
93
|
+
Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
|
|
100
|
+
return unless column_exists?(table_name, :model_id)
|
|
101
|
+
|
|
102
|
+
# Check if we need to rename the string column to avoid collision
|
|
103
|
+
if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
|
|
104
|
+
# Temporarily rename the string column
|
|
105
|
+
rename_column table_name, :model_id, :model_id_string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Add the foreign key reference
|
|
109
|
+
unless column_exists?(table_name, "#{foreign_key_name}_id")
|
|
110
|
+
add_reference table_name, foreign_key_name, foreign_key: true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
say_with_time "Migrating #{table_name} model references" do
|
|
114
|
+
record_class.reset_column_information
|
|
115
|
+
has_provider = column_exists?(table_name, :provider)
|
|
116
|
+
|
|
117
|
+
# Determine which column to read from (renamed or original)
|
|
118
|
+
model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
|
|
119
|
+
|
|
120
|
+
record_class.find_each do |record|
|
|
121
|
+
model_id = record.read_attribute(model_id_column)
|
|
122
|
+
next if model_id.blank?
|
|
123
|
+
|
|
124
|
+
provider = has_provider ? record.read_attribute(:provider) : nil
|
|
125
|
+
|
|
126
|
+
model = if has_provider && provider.present?
|
|
127
|
+
model_class.find_by(model_id: model_id, provider: provider)
|
|
128
|
+
else
|
|
129
|
+
find_model_for_record(model_id, model_class)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
record.update_column("#{foreign_key_name}_id", model.id) if model
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_model_for_record(model_id, model_class)
|
|
138
|
+
begin
|
|
139
|
+
model_info, _provider = LexLLM.models.resolve(model_id)
|
|
140
|
+
model_class.find_by(model_id: model_info.id, provider: model_info.provider)
|
|
141
|
+
rescue => e
|
|
142
|
+
model_class.find_by(model_id: model_id)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../generator_helpers'
|
|
6
|
+
|
|
7
|
+
module LexLLM
|
|
8
|
+
module Generators
|
|
9
|
+
# Generator to upgrade existing LexLLM apps to v1.7 with new Rails-like API
|
|
10
|
+
class UpgradeToV17Generator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
include LexLLM::Generators::GeneratorHelpers
|
|
13
|
+
|
|
14
|
+
namespace 'lex_llm:upgrade_to_v1_7'
|
|
15
|
+
source_root File.expand_path('templates', __dir__)
|
|
16
|
+
|
|
17
|
+
# Override source_paths to include install templates
|
|
18
|
+
def self.source_paths
|
|
19
|
+
[
|
|
20
|
+
File.expand_path('templates', __dir__),
|
|
21
|
+
File.expand_path('../install/templates', __dir__)
|
|
22
|
+
]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
|
|
26
|
+
|
|
27
|
+
desc 'Upgrades existing LexLLM apps to v1.7 with new Rails-like API\n' \
|
|
28
|
+
'Usage: bin/rails g lex_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
|
|
29
|
+
|
|
30
|
+
def self.next_migration_number(dirname)
|
|
31
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_migration_file
|
|
35
|
+
@model_table_already_existed = table_exists?(table_name_for(model_model_name))
|
|
36
|
+
|
|
37
|
+
# First check if models table exists, if not create it
|
|
38
|
+
unless @model_table_already_existed
|
|
39
|
+
migration_template 'create_models_migration.rb.tt',
|
|
40
|
+
"db/migrate/create_#{table_name_for(model_model_name)}.rb",
|
|
41
|
+
migration_version: migration_version,
|
|
42
|
+
model_model_name: model_model_name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
migration_template 'migration.rb.tt',
|
|
46
|
+
'db/migrate/migrate_to_lex_llm_model_references.rb',
|
|
47
|
+
migration_version: migration_version,
|
|
48
|
+
chat_model_name: chat_model_name,
|
|
49
|
+
message_model_name: message_model_name,
|
|
50
|
+
tool_call_model_name: tool_call_model_name,
|
|
51
|
+
model_model_name: model_model_name,
|
|
52
|
+
model_table_already_existed: @model_table_already_existed
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create_model_file
|
|
56
|
+
create_namespace_modules
|
|
57
|
+
|
|
58
|
+
template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update_existing_models
|
|
62
|
+
update_model_acts_as(chat_model_name, 'acts_as_chat', acts_as_chat_declaration)
|
|
63
|
+
update_model_acts_as(message_model_name, 'acts_as_message', acts_as_message_declaration)
|
|
64
|
+
update_model_acts_as(tool_call_model_name, 'acts_as_tool_call', acts_as_tool_call_declaration)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update_initializer
|
|
68
|
+
initializer_path = 'config/initializers/lex_llm.rb'
|
|
69
|
+
|
|
70
|
+
unless File.exist?(initializer_path)
|
|
71
|
+
say_status :warning, 'No initializer found. Creating one...', :yellow
|
|
72
|
+
template 'initializer.rb.tt', initializer_path
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
initializer_content = File.read(initializer_path)
|
|
77
|
+
|
|
78
|
+
return if initializer_content.include?('config.use_new_acts_as')
|
|
79
|
+
|
|
80
|
+
inject_into_file initializer_path, after: /^LexLLM\.configure do \|config\|\n/ do
|
|
81
|
+
lines = [' # Enable the new Rails-like API', ' config.use_new_acts_as = true']
|
|
82
|
+
lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
|
|
83
|
+
lines << ''
|
|
84
|
+
lines.join("\n")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def show_next_steps
|
|
89
|
+
say_status :success, 'Upgrade prepared!', :green
|
|
90
|
+
say <<~INSTRUCTIONS
|
|
91
|
+
|
|
92
|
+
Next steps:
|
|
93
|
+
1. Review the generated migrations
|
|
94
|
+
2. Run: bin/rails db:migrate
|
|
95
|
+
3. Update your code to use the new API: #{chat_model_name}.create! now has the same signature as LexLLM.chat
|
|
96
|
+
|
|
97
|
+
⚠️ If you get "undefined method 'acts_as_model'" during migration:
|
|
98
|
+
Add this to config/application.rb BEFORE your Application class:
|
|
99
|
+
|
|
100
|
+
LexLLM.configure do |config|
|
|
101
|
+
config.use_new_acts_as = true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
📚 See the full migration guide: https://github.com/LegionIO/lex-llm
|
|
105
|
+
|
|
106
|
+
INSTRUCTIONS
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def update_model_acts_as(model_name, old_acts_as, new_acts_as)
|
|
112
|
+
model_path = "app/models/#{model_name.underscore}.rb"
|
|
113
|
+
return unless File.exist?(Rails.root.join(model_path))
|
|
114
|
+
|
|
115
|
+
content = File.read(Rails.root.join(model_path))
|
|
116
|
+
return unless content.match?(/^\s*#{old_acts_as}/)
|
|
117
|
+
|
|
118
|
+
gsub_file model_path, /^\s*#{old_acts_as}.*$/, " #{new_acts_as}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class AddLexLlmV19Columns < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
unless column_exists?(:<%= message_table_name %>, :cached_tokens)
|
|
4
|
+
add_column :<%= message_table_name %>, :cached_tokens, :integer
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
unless column_exists?(:<%= message_table_name %>, :cache_creation_tokens)
|
|
8
|
+
add_column :<%= message_table_name %>, :cache_creation_tokens, :integer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless column_exists?(:<%= message_table_name %>, :content_raw)
|
|
12
|
+
add_column :<%= message_table_name %>, :content_raw, :json
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
require_relative '../generator_helpers'
|
|
6
|
+
|
|
7
|
+
module LexLLM
|
|
8
|
+
module Generators
|
|
9
|
+
# Generator to add v1.9 columns (cached tokens + raw content support) to existing apps.
|
|
10
|
+
class UpgradeToV19Generator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
include LexLLM::Generators::GeneratorHelpers
|
|
13
|
+
|
|
14
|
+
namespace 'lex_llm:upgrade_to_v1_9'
|
|
15
|
+
source_root File.expand_path('templates', __dir__)
|
|
16
|
+
|
|
17
|
+
argument :model_mappings, type: :array, default: [], banner: 'message:MessageName'
|
|
18
|
+
|
|
19
|
+
desc 'Adds cached token columns and raw content storage fields introduced in v1.9.0'
|
|
20
|
+
|
|
21
|
+
def self.next_migration_number(dirname)
|
|
22
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_migration_file
|
|
26
|
+
parse_model_mappings
|
|
27
|
+
|
|
28
|
+
migration_template 'add_v1_9_message_columns.rb.tt',
|
|
29
|
+
'db/migrate/add_lex_llm_v1_9_columns.rb',
|
|
30
|
+
migration_version: migration_version,
|
|
31
|
+
message_table_name: message_table_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def show_next_steps
|
|
35
|
+
say_status :success, 'Upgrade prepared!', :green
|
|
36
|
+
say <<~INSTRUCTIONS
|
|
37
|
+
|
|
38
|
+
Next steps:
|
|
39
|
+
1. Review the generated migration
|
|
40
|
+
2. Run: bin/rails db:migrate
|
|
41
|
+
3. Restart your application server
|
|
42
|
+
|
|
43
|
+
📚 See the v1.9.0 release notes for details on cached token tracking and raw content support.
|
|
44
|
+
|
|
45
|
+
INSTRUCTIONS
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Builds shared provider defaults for lex-llm-* extension gems.
|
|
7
|
+
module ProviderSettings
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def build(family:, instance: {}, enabled: true, discovery: {}, instances: {})
|
|
11
|
+
deep_merge(
|
|
12
|
+
Legion::Extensions::Llm.default_settings,
|
|
13
|
+
{
|
|
14
|
+
enabled: enabled,
|
|
15
|
+
provider_family: family,
|
|
16
|
+
discovery: deep_merge({ enabled: true, interval_seconds: 300 }, discovery || {}),
|
|
17
|
+
instances: deep_merge(
|
|
18
|
+
{
|
|
19
|
+
default: deep_merge(
|
|
20
|
+
{ enabled: true, credentials: nil, fleet: { enabled: false, consumer_priority: 0, prefetch: 1 } },
|
|
21
|
+
instance || {}
|
|
22
|
+
)
|
|
23
|
+
},
|
|
24
|
+
instances || {}
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deep_dup(value)
|
|
31
|
+
case value
|
|
32
|
+
when Hash
|
|
33
|
+
value.to_h { |key, inner_value| [key, deep_dup(inner_value)] }
|
|
34
|
+
when Array
|
|
35
|
+
value.map { |inner_value| deep_dup(inner_value) }
|
|
36
|
+
else
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deep_merge(left, right)
|
|
42
|
+
deep_dup(left || {}).merge(deep_dup(right || {})) do |_key, left_value, right_value|
|
|
43
|
+
left_value.is_a?(Hash) && right_value.is_a?(Hash) ? deep_merge(left_value, right_value) : right_value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Transport
|
|
7
|
+
# Shared RabbitMQ live-work lane defaults for provider fleet workers.
|
|
8
|
+
module FleetLane
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
queue_expires_ms: 60_000,
|
|
11
|
+
message_ttl_ms: 120_000,
|
|
12
|
+
queue_max_length: 100,
|
|
13
|
+
delivery_limit: 3,
|
|
14
|
+
consumer_ack_timeout_ms: 300_000
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def queue_options(settings = {})
|
|
20
|
+
config = DEFAULTS.merge((settings || {}).compact.transform_keys(&:to_sym))
|
|
21
|
+
{
|
|
22
|
+
durable: true,
|
|
23
|
+
auto_delete: false,
|
|
24
|
+
arguments: queue_arguments(config)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def queue_arguments(config)
|
|
29
|
+
{
|
|
30
|
+
'x-queue-type' => 'quorum',
|
|
31
|
+
'x-queue-leader-locator' => 'balanced',
|
|
32
|
+
'x-expires' => config.fetch(:queue_expires_ms),
|
|
33
|
+
'x-message-ttl' => config.fetch(:message_ttl_ms),
|
|
34
|
+
'x-overflow' => 'reject-publish',
|
|
35
|
+
'x-max-length' => config.fetch(:queue_max_length),
|
|
36
|
+
'x-delivery-limit' => config.fetch(:delivery_limit),
|
|
37
|
+
'x-consumer-timeout' => config.fetch(:consumer_ack_timeout_ms)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_queue_class(queue_name:, exchange_class:, routing_key: queue_name, base_queue_class: nil,
|
|
42
|
+
settings: {})
|
|
43
|
+
parent = base_queue_class || legion_queue_class
|
|
44
|
+
unless parent
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
'base_queue_class is required when Legion::Transport::Queue is not loaded'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
options = queue_options(settings)
|
|
50
|
+
Class.new(parent) do
|
|
51
|
+
define_method(:queue_name) { queue_name }
|
|
52
|
+
define_method(:queue_options) { options }
|
|
53
|
+
define_method(:dlx_enabled) { false }
|
|
54
|
+
define_method(:initialize) do
|
|
55
|
+
super()
|
|
56
|
+
bind(exchange_class.new, routing_key: routing_key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def legion_queue_class
|
|
62
|
+
return nil unless defined?(::Legion::Transport::Queue)
|
|
63
|
+
|
|
64
|
+
::Legion::Transport::Queue
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lex_llm'
|
|
4
|
+
require 'legion/extensions/llm/provider_settings'
|
|
5
|
+
require 'legion/extensions/llm/transport/fleet_lane'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
# Legion-native namespace for the shared LLM provider framework.
|
|
10
|
+
module Llm
|
|
11
|
+
VERSION = LexLLM::VERSION unless const_defined?(:VERSION, false)
|
|
12
|
+
|
|
13
|
+
# Provider-neutral value objects exposed under the Legion extension namespace.
|
|
14
|
+
module Types
|
|
15
|
+
ModelOffering = LexLLM::Routing::ModelOffering unless const_defined?(:ModelOffering, false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Shared routing helpers exposed under the Legion extension namespace.
|
|
19
|
+
module Routing
|
|
20
|
+
LaneKey = LexLLM::Routing::LaneKey unless const_defined?(:LaneKey, false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.default_settings
|
|
24
|
+
{
|
|
25
|
+
fleet: {
|
|
26
|
+
enabled: false,
|
|
27
|
+
scheduler: :basic_get,
|
|
28
|
+
consumer_priority: 0,
|
|
29
|
+
queue_expires_ms: 60_000,
|
|
30
|
+
message_ttl_ms: 120_000,
|
|
31
|
+
queue_max_length: 100,
|
|
32
|
+
delivery_limit: 3,
|
|
33
|
+
consumer_ack_timeout_ms: 300_000,
|
|
34
|
+
endpoint: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
empty_lane_backoff_ms: 250,
|
|
37
|
+
idle_backoff_ms: 1_000,
|
|
38
|
+
max_consecutive_pulls_per_lane: 0,
|
|
39
|
+
accept_when: []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.provider_settings(...)
|
|
46
|
+
ProviderSettings.build(...)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Adds chat and message persistence capabilities to ActiveRecord models.
|
|
6
|
+
module ActsAs
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
# When ActsAs is included, ensure models are loaded from database
|
|
10
|
+
def self.included(base)
|
|
11
|
+
super
|
|
12
|
+
# Monkey-patch Models to use database when ActsAs is active
|
|
13
|
+
LexLLM::Models.class_eval do
|
|
14
|
+
def self.load_models
|
|
15
|
+
database_models = read_from_database
|
|
16
|
+
return database_models if database_models.any?
|
|
17
|
+
|
|
18
|
+
LexLLM.logger.debug { 'Model registry is empty in database, falling back to JSON registry' }
|
|
19
|
+
read_from_json
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
LexLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
|
|
22
|
+
read_from_json
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.read_from_database
|
|
26
|
+
model_class = LexLLM.config.model_registry_class
|
|
27
|
+
model_class = model_class.constantize if model_class.is_a?(String)
|
|
28
|
+
return [] unless model_class.table_exists?
|
|
29
|
+
|
|
30
|
+
model_class.all.map(&:to_llm)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load_from_database!
|
|
34
|
+
@models = self.class.read_from_database
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
|
40
|
+
def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
|
|
41
|
+
model: :model, model_class: nil, model_foreign_key: nil)
|
|
42
|
+
include LexLLM::ActiveRecord::ChatMethods
|
|
43
|
+
|
|
44
|
+
class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
|
|
45
|
+
|
|
46
|
+
self.messages_association_name = messages
|
|
47
|
+
self.model_association_name = model
|
|
48
|
+
self.message_class = (message_class || messages.to_s.classify).to_s
|
|
49
|
+
self.model_class = (model_class || model.to_s.classify).to_s
|
|
50
|
+
|
|
51
|
+
has_many messages,
|
|
52
|
+
-> { order(created_at: :asc) },
|
|
53
|
+
class_name: self.message_class,
|
|
54
|
+
foreign_key: messages_foreign_key,
|
|
55
|
+
dependent: :destroy
|
|
56
|
+
|
|
57
|
+
belongs_to model,
|
|
58
|
+
class_name: self.model_class,
|
|
59
|
+
foreign_key: model_foreign_key,
|
|
60
|
+
optional: true
|
|
61
|
+
|
|
62
|
+
define_method :messages_association do
|
|
63
|
+
send(messages_association_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
define_method :model_association do
|
|
67
|
+
send(model_association_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
define_method :'model_association=' do |value|
|
|
71
|
+
send("#{model_association_name}=", value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def acts_as_model(chats: :chats, chat_class: nil, chats_foreign_key: nil)
|
|
76
|
+
include LexLLM::ActiveRecord::ModelMethods
|
|
77
|
+
|
|
78
|
+
class_attribute :chats_association_name, :chat_class
|
|
79
|
+
|
|
80
|
+
self.chats_association_name = chats
|
|
81
|
+
self.chat_class = (chat_class || chats.to_s.classify).to_s
|
|
82
|
+
|
|
83
|
+
validates :model_id, presence: true, uniqueness: { scope: :provider }
|
|
84
|
+
validates :provider, presence: true
|
|
85
|
+
validates :name, presence: true
|
|
86
|
+
|
|
87
|
+
has_many chats, class_name: self.chat_class, foreign_key: chats_foreign_key
|
|
88
|
+
|
|
89
|
+
define_method :chats_association do
|
|
90
|
+
send(chats_association_name)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
|
|
95
|
+
tool_calls: :tool_calls, tool_call_class: nil, tool_calls_foreign_key: nil,
|
|
96
|
+
model: :model, model_class: nil, model_foreign_key: nil)
|
|
97
|
+
include LexLLM::ActiveRecord::MessageMethods
|
|
98
|
+
|
|
99
|
+
class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
|
|
100
|
+
:chat_class, :tool_call_class, :model_class
|
|
101
|
+
|
|
102
|
+
self.chat_association_name = chat
|
|
103
|
+
self.tool_calls_association_name = tool_calls
|
|
104
|
+
self.model_association_name = model
|
|
105
|
+
self.chat_class = (chat_class || chat.to_s.classify).to_s
|
|
106
|
+
self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
|
|
107
|
+
self.model_class = (model_class || model.to_s.classify).to_s
|
|
108
|
+
|
|
109
|
+
belongs_to chat,
|
|
110
|
+
class_name: self.chat_class,
|
|
111
|
+
foreign_key: chat_foreign_key,
|
|
112
|
+
touch: touch_chat
|
|
113
|
+
|
|
114
|
+
has_many tool_calls,
|
|
115
|
+
class_name: self.tool_call_class,
|
|
116
|
+
foreign_key: tool_calls_foreign_key,
|
|
117
|
+
dependent: :destroy
|
|
118
|
+
|
|
119
|
+
belongs_to :parent_tool_call,
|
|
120
|
+
class_name: self.tool_call_class,
|
|
121
|
+
foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
|
|
122
|
+
optional: true
|
|
123
|
+
|
|
124
|
+
has_many :tool_results,
|
|
125
|
+
through: tool_calls,
|
|
126
|
+
source: :result,
|
|
127
|
+
class_name: name
|
|
128
|
+
|
|
129
|
+
belongs_to model,
|
|
130
|
+
class_name: self.model_class,
|
|
131
|
+
foreign_key: model_foreign_key,
|
|
132
|
+
optional: true
|
|
133
|
+
|
|
134
|
+
delegate :tool_call?, :tool_result?, to: :to_llm
|
|
135
|
+
|
|
136
|
+
define_method :chat_association do
|
|
137
|
+
send(chat_association_name)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
define_method :tool_calls_association do
|
|
141
|
+
send(tool_calls_association_name)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
define_method :model_association do
|
|
145
|
+
send(model_association_name)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
|
|
150
|
+
result: :result, result_class: nil, result_foreign_key: nil)
|
|
151
|
+
include LexLLM::ActiveRecord::ToolCallMethods
|
|
152
|
+
|
|
153
|
+
class_attribute :message_association_name, :result_association_name, :message_class, :result_class
|
|
154
|
+
|
|
155
|
+
self.message_association_name = message
|
|
156
|
+
self.result_association_name = result
|
|
157
|
+
self.message_class = (message_class || message.to_s.classify).to_s
|
|
158
|
+
self.result_class = (result_class || self.message_class).to_s
|
|
159
|
+
|
|
160
|
+
belongs_to message,
|
|
161
|
+
class_name: self.message_class,
|
|
162
|
+
foreign_key: message_foreign_key
|
|
163
|
+
|
|
164
|
+
has_one result,
|
|
165
|
+
class_name: self.result_class,
|
|
166
|
+
foreign_key: result_foreign_key,
|
|
167
|
+
dependent: :nullify
|
|
168
|
+
|
|
169
|
+
define_method :message_association do
|
|
170
|
+
send(message_association_name)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
define_method :result_association do
|
|
174
|
+
send(result_association_name)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|