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,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm/schema'
4
+
5
+ module LexLLM
6
+ # Parameter definition for Tool methods.
7
+ class Parameter
8
+ attr_reader :name, :type, :description, :required
9
+
10
+ def initialize(name, type: 'string', desc: nil, required: true)
11
+ @name = name
12
+ @type = type
13
+ @description = desc
14
+ @required = required
15
+ end
16
+ end
17
+
18
+ # Base class for creating tools that AI models can use
19
+ class Tool
20
+ # Stops conversation continuation after tool execution
21
+ class Halt
22
+ attr_reader :content
23
+
24
+ def initialize(content)
25
+ @content = content
26
+ end
27
+
28
+ def to_s
29
+ @content.to_s
30
+ end
31
+ end
32
+
33
+ class << self
34
+ attr_reader :params_schema_definition
35
+
36
+ def description(text = nil)
37
+ return @description unless text
38
+
39
+ @description = text
40
+ end
41
+
42
+ def param(name, **)
43
+ parameters[name] = Parameter.new(name, **)
44
+ end
45
+
46
+ def parameters
47
+ @parameters ||= {}
48
+ end
49
+
50
+ def params(schema = nil, &block)
51
+ @params_schema_definition = SchemaDefinition.new(schema:, block:)
52
+ self
53
+ end
54
+
55
+ def with_params(**params)
56
+ @provider_params = params
57
+ self
58
+ end
59
+
60
+ def provider_params
61
+ @provider_params ||= {}
62
+ end
63
+ end
64
+
65
+ def name
66
+ klass_name = self.class.name
67
+ normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
68
+ normalized.encode('ASCII', replace: '')
69
+ .gsub(/[^a-zA-Z0-9_-]/, '-')
70
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
71
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
72
+ .downcase
73
+ .delete_suffix('_tool')
74
+ end
75
+
76
+ def description
77
+ self.class.description
78
+ end
79
+
80
+ def parameters
81
+ self.class.parameters
82
+ end
83
+
84
+ def provider_params
85
+ self.class.provider_params
86
+ end
87
+
88
+ def params_schema
89
+ return @params_schema if defined?(@params_schema)
90
+
91
+ @params_schema = begin
92
+ definition = self.class.params_schema_definition
93
+ if definition&.present?
94
+ definition.json_schema
95
+ elsif parameters.any?
96
+ SchemaDefinition.from_parameters(parameters)&.json_schema
97
+ end
98
+ end
99
+ end
100
+
101
+ def call(args)
102
+ normalized_args = normalize_args(args)
103
+ validation_error = validate_keyword_arguments(normalized_args)
104
+ return { error: "Invalid tool arguments: #{validation_error}" } if validation_error
105
+
106
+ LexLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
107
+ result = execute(**normalized_args)
108
+ LexLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
109
+ result
110
+ end
111
+
112
+ def execute(...)
113
+ raise NotImplementedError, 'Subclasses must implement #execute'
114
+ end
115
+
116
+ protected
117
+
118
+ def halt(message)
119
+ Halt.new(message)
120
+ end
121
+
122
+ def normalize_args(args)
123
+ return {} if args.nil?
124
+ return args.transform_keys(&:to_sym) if args.respond_to?(:transform_keys)
125
+
126
+ {}
127
+ end
128
+
129
+ def validate_keyword_arguments(arguments)
130
+ required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
131
+
132
+ return nil if required_keywords.empty? && optional_keywords.empty?
133
+
134
+ argument_keys = arguments.keys
135
+ missing_keyword = first_missing_keyword(required_keywords, argument_keys)
136
+ return "missing keyword: #{missing_keyword}" if missing_keyword
137
+ return nil if accepts_extra_keywords
138
+
139
+ allowed_keywords = required_keywords + optional_keywords
140
+ unknown_keyword = first_unknown_keyword(argument_keys, allowed_keywords)
141
+ return "unknown keyword: #{unknown_keyword}" if unknown_keyword
142
+
143
+ nil
144
+ end
145
+
146
+ def execute_keyword_signature
147
+ keyword_signature = method(:execute).parameters
148
+ required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
149
+ optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
150
+ accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
151
+
152
+ [required_keywords, optional_keywords, accepts_extra_keywords]
153
+ end
154
+
155
+ def first_missing_keyword(required_keywords, argument_keys)
156
+ (required_keywords - argument_keys).first
157
+ end
158
+
159
+ def first_unknown_keyword(argument_keys, allowed_keywords)
160
+ (argument_keys - allowed_keywords).first
161
+ end
162
+
163
+ # Wraps schema handling for tool parameters, supporting JSON Schema hashes,
164
+ # LexLLM::Schema instances/classes, and DSL blocks.
165
+ class SchemaDefinition
166
+ def self.from_parameters(parameters)
167
+ return nil if parameters.nil? || parameters.empty?
168
+
169
+ properties = parameters.to_h do |name, param|
170
+ schema = {
171
+ type: map_type(param.type),
172
+ description: param.description
173
+ }.compact
174
+
175
+ schema[:items] = default_items_schema if schema[:type] == 'array'
176
+
177
+ [name.to_s, schema]
178
+ end
179
+
180
+ required = parameters.select { |_, param| param.required }.keys.map(&:to_s)
181
+
182
+ json_schema = {
183
+ type: 'object',
184
+ properties: properties,
185
+ required: required,
186
+ additionalProperties: false,
187
+ strict: true
188
+ }
189
+
190
+ new(schema: json_schema)
191
+ end
192
+
193
+ def self.map_type(type)
194
+ case type.to_s
195
+ when 'integer', 'int' then 'integer'
196
+ when 'number', 'float', 'double' then 'number'
197
+ when 'boolean' then 'boolean'
198
+ when 'array' then 'array'
199
+ when 'object' then 'object'
200
+ else
201
+ 'string'
202
+ end
203
+ end
204
+
205
+ def self.default_items_schema
206
+ { type: 'string' }
207
+ end
208
+
209
+ def initialize(schema: nil, block: nil)
210
+ @schema = schema
211
+ @block = block
212
+ end
213
+
214
+ def present?
215
+ @schema || @block
216
+ end
217
+
218
+ def json_schema
219
+ @json_schema ||= LexLLM::Utils.deep_stringify_keys(resolve_schema)
220
+ end
221
+
222
+ private
223
+
224
+ def resolve_schema
225
+ return resolve_direct_schema(@schema) if @schema
226
+ return build_from_block(&@block) if @block
227
+
228
+ nil
229
+ end
230
+
231
+ def resolve_direct_schema(schema)
232
+ return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema)
233
+ return LexLLM::Utils.deep_dup(schema) if schema.is_a?(Hash)
234
+ if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
235
+ return extract_schema(schema.new.to_json_schema)
236
+ end
237
+
238
+ nil
239
+ end
240
+
241
+ def build_from_block(&)
242
+ schema_class = LexLLM::Schema.create(&)
243
+ extract_schema(schema_class.new.to_json_schema)
244
+ end
245
+
246
+ def extract_schema(schema_hash)
247
+ return nil unless schema_hash.is_a?(Hash)
248
+
249
+ schema = schema_hash[:schema] || schema_hash['schema'] || schema_hash
250
+ LexLLM::Utils.deep_dup(schema)
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ # Represents a function call from an AI model to a Tool.
5
+ class ToolCall
6
+ attr_reader :id, :name, :arguments
7
+ attr_accessor :thought_signature
8
+
9
+ def initialize(id:, name:, arguments: {}, thought_signature: nil)
10
+ @id = id
11
+ @name = name
12
+ @arguments = arguments
13
+ @thought_signature = thought_signature
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ id: @id,
19
+ name: @name,
20
+ arguments: @arguments,
21
+ thought_signature: @thought_signature
22
+ }.compact
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ # Represents a transcription of audio content.
5
+ class Transcription
6
+ attr_reader :text, :model, :language, :duration, :segments, :input_tokens, :output_tokens
7
+
8
+ def initialize(text:, model:, **attributes)
9
+ @text = text
10
+ @model = model
11
+ @language = attributes[:language]
12
+ @duration = attributes[:duration]
13
+ @segments = attributes[:segments]
14
+ @input_tokens = attributes[:input_tokens]
15
+ @output_tokens = attributes[:output_tokens]
16
+ end
17
+
18
+ def self.transcribe(audio_file, **kwargs)
19
+ model = kwargs.delete(:model)
20
+ language = kwargs.delete(:language)
21
+ provider = kwargs.delete(:provider)
22
+ assume_model_exists = kwargs.delete(:assume_model_exists) { false }
23
+ context = kwargs.delete(:context)
24
+ options = kwargs
25
+
26
+ config = context&.config || LexLLM.config
27
+ model ||= config.default_transcription_model
28
+ model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
29
+ config: config)
30
+ model_id = model.id
31
+
32
+ provider_instance.transcribe(audio_file, model: model_id, language:, **options)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ # Provides utility functions for data manipulation within the LexLLM library
5
+ module Utils
6
+ module_function
7
+
8
+ def hash_get(hash, key)
9
+ hash[key.to_sym] || hash[key.to_s]
10
+ end
11
+
12
+ def to_safe_array(item)
13
+ case item
14
+ when Array
15
+ item
16
+ when Hash
17
+ [item]
18
+ else
19
+ Array(item)
20
+ end
21
+ end
22
+
23
+ def to_time(value)
24
+ return unless value
25
+
26
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
27
+ end
28
+
29
+ def to_date(value)
30
+ return unless value
31
+
32
+ value.is_a?(Date) ? value : Date.parse(value.to_s)
33
+ end
34
+
35
+ def deep_merge(original, overrides)
36
+ original.merge(overrides) do |_key, original_value, overrides_value|
37
+ if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
38
+ deep_merge(original_value, overrides_value)
39
+ else
40
+ overrides_value
41
+ end
42
+ end
43
+ end
44
+
45
+ def deep_dup(value)
46
+ case value
47
+ when Hash
48
+ value.each_with_object({}) do |(key, val), duped|
49
+ duped[deep_dup(key)] = deep_dup(val)
50
+ end
51
+ when Array
52
+ value.map { |item| deep_dup(item) }
53
+ else
54
+ begin
55
+ value.dup
56
+ rescue TypeError
57
+ value
58
+ end
59
+ end
60
+ end
61
+
62
+ def deep_stringify_keys(value)
63
+ case value
64
+ when Hash
65
+ value.each_with_object({}) do |(key, val), result|
66
+ result[key.to_s] = deep_stringify_keys(val)
67
+ end
68
+ when Array
69
+ value.map { |item| deep_stringify_keys(item) }
70
+ when Symbol
71
+ value.to_s
72
+ else
73
+ value
74
+ end
75
+ end
76
+
77
+ def deep_symbolize_keys(value)
78
+ case value
79
+ when Hash
80
+ value.each_with_object({}) do |(key, val), result|
81
+ symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
82
+ result[symbolized_key] = deep_symbolize_keys(val)
83
+ end
84
+ when Array
85
+ value.map { |item| deep_symbolize_keys(item) }
86
+ else
87
+ value
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ VERSION = '0.1.1'
5
+ end
data/lib/lex_llm.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'digest/sha1'
5
+ require 'event_stream_parser'
6
+ require 'faraday'
7
+ require 'faraday/multipart'
8
+ require 'faraday/retry'
9
+ require 'legion/json'
10
+ require 'logger'
11
+ require 'marcel'
12
+ require 'securerandom'
13
+ require 'date'
14
+ require 'time'
15
+ require 'ruby_llm/schema'
16
+ require 'zeitwerk'
17
+
18
+ # Shared LegionIO LLM provider framework.
19
+ module LexLLM
20
+ loader = Zeitwerk::Loader.for_gem
21
+ loader.inflector.inflect(
22
+ 'UI' => 'UI',
23
+ 'api' => 'API',
24
+ 'llm' => 'LLM',
25
+ 'pdf' => 'PDF',
26
+ 'lex_llm' => 'LexLLM'
27
+ )
28
+ loader.ignore("#{__dir__}/tasks")
29
+ loader.ignore("#{__dir__}/generators")
30
+ loader.ignore("#{__dir__}/lex_llm.rb")
31
+ loader.ignore("#{__dir__}/legion")
32
+ loader.ignore("#{__dir__}/lex_llm/railtie.rb")
33
+ loader.setup
34
+
35
+ Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
36
+
37
+ class Error < StandardError; end
38
+
39
+ class << self
40
+ def context
41
+ context_config = config.dup
42
+ yield context_config if block_given?
43
+ Context.new(context_config)
44
+ end
45
+
46
+ def chat(...)
47
+ Chat.new(...)
48
+ end
49
+
50
+ def embed(...)
51
+ Embedding.embed(...)
52
+ end
53
+
54
+ def moderate(...)
55
+ Moderation.moderate(...)
56
+ end
57
+
58
+ def paint(...)
59
+ Image.paint(...)
60
+ end
61
+
62
+ def transcribe(...)
63
+ Transcription.transcribe(...)
64
+ end
65
+
66
+ def models
67
+ Models.instance
68
+ end
69
+
70
+ def providers
71
+ Provider.providers.values
72
+ end
73
+
74
+ def configure
75
+ yield config
76
+ end
77
+
78
+ def config
79
+ @config ||= Configuration.new
80
+ end
81
+
82
+ def logger
83
+ @logger ||= config.logger || Logger.new(
84
+ config.log_file,
85
+ progname: 'LexLLM',
86
+ level: config.log_level
87
+ )
88
+ end
89
+ end
90
+ end
91
+
92
+ if defined?(Rails::Railtie)
93
+ require 'lex_llm/railtie'
94
+ require 'lex_llm/active_record/acts_as'
95
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ def run_test_queue_rspec
4
+ workers = ENV.fetch('RSPEC_WORKERS', nil)
5
+ env = {}
6
+ env['TEST_QUEUE_WORKERS'] = workers if workers && !workers.empty? && ENV.fetch('TEST_QUEUE_WORKERS', '').empty?
7
+
8
+ system(env, 'bundle', 'exec', 'bin/rspec-queue')
9
+ end
10
+
11
+ namespace :lex_llm do
12
+ desc 'Load models from models.json into the database'
13
+ task load_models: :environment do
14
+ if LexLLM.config.model_registry_class
15
+ LexLLM.models.load_from_json!
16
+ model_class = LexLLM.config.model_registry_class.constantize
17
+ model_class.save_to_database
18
+ puts "✅ Loaded #{model_class.count} models into database"
19
+ else
20
+ puts 'Model registry not configured. Run bin/rails generate lex_llm:install'
21
+ end
22
+ end
23
+ end