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
data/lib/lex_llm/tool.rb
ADDED
|
@@ -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
|
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
|