lex-llm 0.1.2 → 0.1.4
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +12 -1
- data/Gemfile +1 -19
- data/README.md +25 -26
- data/lex-llm.gemspec +2 -2
- data/lib/legion/extensions/llm/agent.rb +366 -0
- data/lib/legion/extensions/llm/aliases.rb +42 -0
- data/lib/legion/extensions/llm/attachment.rb +229 -0
- data/lib/legion/extensions/llm/chat.rb +355 -0
- data/lib/legion/extensions/llm/chunk.rb +10 -0
- data/lib/legion/extensions/llm/configuration.rb +82 -0
- data/lib/legion/extensions/llm/connection.rb +134 -0
- data/lib/legion/extensions/llm/content.rb +81 -0
- data/lib/legion/extensions/llm/context.rb +33 -0
- data/lib/legion/extensions/llm/embedding.rb +33 -0
- data/lib/legion/extensions/llm/error.rb +116 -0
- data/lib/legion/extensions/llm/image.rb +109 -0
- data/lib/legion/extensions/llm/message.rb +111 -0
- data/lib/legion/extensions/llm/mime_type.rb +75 -0
- data/lib/legion/extensions/llm/model/info.rb +117 -0
- data/lib/legion/extensions/llm/model/modalities.rb +26 -0
- data/lib/legion/extensions/llm/model/pricing.rb +52 -0
- data/lib/legion/extensions/llm/model/pricing_category.rb +50 -0
- data/lib/legion/extensions/llm/model/pricing_tier.rb +37 -0
- data/lib/legion/extensions/llm/model.rb +11 -0
- data/lib/legion/extensions/llm/models.rb +514 -0
- data/lib/{lex_llm → legion/extensions/llm}/models_schema.json +1 -1
- data/lib/legion/extensions/llm/moderation.rb +60 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +276 -0
- data/lib/legion/extensions/llm/provider.rb +337 -0
- data/lib/legion/extensions/llm/routing/lane_key.rb +57 -0
- data/lib/legion/extensions/llm/routing/model_offering.rb +173 -0
- data/lib/legion/extensions/llm/routing.rb +11 -0
- data/lib/legion/extensions/llm/stream_accumulator.rb +209 -0
- data/lib/legion/extensions/llm/streaming.rb +181 -0
- data/lib/legion/extensions/llm/thinking.rb +53 -0
- data/lib/legion/extensions/llm/tokens.rb +51 -0
- data/lib/legion/extensions/llm/tool.rb +258 -0
- data/lib/legion/extensions/llm/tool_call.rb +29 -0
- data/lib/legion/extensions/llm/transcription.rb +39 -0
- data/lib/legion/extensions/llm/utils.rb +95 -0
- data/lib/legion/extensions/llm/version.rb +9 -0
- data/lib/legion/extensions/llm.rb +85 -6
- metadata +40 -122
- data/lib/generators/lex_llm/agent/agent_generator.rb +0 -36
- data/lib/generators/lex_llm/agent/templates/agent.rb.tt +0 -6
- data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +0 -256
- data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +0 -38
- data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +0 -12
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +0 -31
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +0 -31
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +0 -27
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +0 -1
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +0 -13
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +0 -23
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +0 -10
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +0 -2
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +0 -4
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +0 -14
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +0 -13
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +0 -17
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +0 -40
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +0 -27
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +0 -29
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +0 -28
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +0 -11
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +0 -25
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +0 -1
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +0 -8
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +0 -21
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +0 -6
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +0 -2
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +0 -4
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +0 -9
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +0 -7
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +0 -8
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +0 -16
- data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +0 -15
- data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +0 -38
- data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +0 -17
- data/lib/generators/lex_llm/generator_helpers.rb +0 -214
- data/lib/generators/lex_llm/install/install_generator.rb +0 -109
- data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +0 -3
- data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +0 -7
- data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +0 -19
- data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/lex_llm/install/templates/initializer.rb.tt +0 -20
- data/lib/generators/lex_llm/install/templates/message_model.rb.tt +0 -4
- data/lib/generators/lex_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/lex_llm/schema/schema_generator.rb +0 -26
- data/lib/generators/lex_llm/schema/templates/schema.rb.tt +0 -2
- data/lib/generators/lex_llm/tool/templates/tool.rb.tt +0 -9
- data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +0 -13
- data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +0 -13
- data/lib/generators/lex_llm/tool/tool_generator.rb +0 -96
- data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/lex_llm/active_record/acts_as.rb +0 -180
- data/lib/lex_llm/active_record/acts_as_legacy.rb +0 -503
- data/lib/lex_llm/active_record/chat_methods.rb +0 -468
- data/lib/lex_llm/active_record/message_methods.rb +0 -131
- data/lib/lex_llm/active_record/model_methods.rb +0 -76
- data/lib/lex_llm/active_record/payload_helpers.rb +0 -26
- data/lib/lex_llm/active_record/tool_call_methods.rb +0 -15
- data/lib/lex_llm/agent.rb +0 -365
- data/lib/lex_llm/aliases.rb +0 -38
- data/lib/lex_llm/attachment.rb +0 -223
- data/lib/lex_llm/chat.rb +0 -351
- data/lib/lex_llm/chunk.rb +0 -6
- data/lib/lex_llm/configuration.rb +0 -81
- data/lib/lex_llm/connection.rb +0 -130
- data/lib/lex_llm/content.rb +0 -77
- data/lib/lex_llm/context.rb +0 -29
- data/lib/lex_llm/embedding.rb +0 -29
- data/lib/lex_llm/error.rb +0 -112
- data/lib/lex_llm/image.rb +0 -105
- data/lib/lex_llm/message.rb +0 -107
- data/lib/lex_llm/mime_type.rb +0 -71
- data/lib/lex_llm/model/info.rb +0 -113
- data/lib/lex_llm/model/modalities.rb +0 -22
- data/lib/lex_llm/model/pricing.rb +0 -48
- data/lib/lex_llm/model/pricing_category.rb +0 -46
- data/lib/lex_llm/model/pricing_tier.rb +0 -33
- data/lib/lex_llm/model.rb +0 -7
- data/lib/lex_llm/models.rb +0 -506
- data/lib/lex_llm/moderation.rb +0 -56
- data/lib/lex_llm/provider/open_ai_compatible.rb +0 -219
- data/lib/lex_llm/provider.rb +0 -278
- data/lib/lex_llm/railtie.rb +0 -35
- data/lib/lex_llm/routing/lane_key.rb +0 -51
- data/lib/lex_llm/routing/model_offering.rb +0 -169
- data/lib/lex_llm/routing.rb +0 -7
- data/lib/lex_llm/stream_accumulator.rb +0 -203
- data/lib/lex_llm/streaming.rb +0 -175
- data/lib/lex_llm/thinking.rb +0 -49
- data/lib/lex_llm/tokens.rb +0 -47
- data/lib/lex_llm/tool.rb +0 -254
- data/lib/lex_llm/tool_call.rb +0 -25
- data/lib/lex_llm/transcription.rb +0 -35
- data/lib/lex_llm/utils.rb +0 -91
- data/lib/lex_llm/version.rb +0 -5
- data/lib/lex_llm.rb +0 -96
- data/lib/tasks/lex_llm.rake +0 -23
- /data/lib/{lex_llm → legion/extensions/llm}/aliases.json +0 -0
- /data/lib/{lex_llm → legion/extensions/llm}/models.json +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm/schema'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Llm
|
|
8
|
+
# Parameter definition for Tool methods.
|
|
9
|
+
class Parameter
|
|
10
|
+
attr_reader :name, :type, :description, :required
|
|
11
|
+
|
|
12
|
+
def initialize(name, type: 'string', desc: nil, required: true)
|
|
13
|
+
@name = name
|
|
14
|
+
@type = type
|
|
15
|
+
@description = desc
|
|
16
|
+
@required = required
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Base class for creating tools that AI models can use
|
|
21
|
+
class Tool
|
|
22
|
+
# Stops conversation continuation after tool execution
|
|
23
|
+
class Halt
|
|
24
|
+
attr_reader :content
|
|
25
|
+
|
|
26
|
+
def initialize(content)
|
|
27
|
+
@content = content
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
@content.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
attr_reader :params_schema_definition
|
|
37
|
+
|
|
38
|
+
def description(text = nil)
|
|
39
|
+
return @description unless text
|
|
40
|
+
|
|
41
|
+
@description = text
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def param(name, **)
|
|
45
|
+
parameters[name] = Parameter.new(name, **)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parameters
|
|
49
|
+
@parameters ||= {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def params(schema = nil, &block)
|
|
53
|
+
@params_schema_definition = SchemaDefinition.new(schema:, block:)
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def with_params(**params)
|
|
58
|
+
@provider_params = params
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def provider_params
|
|
63
|
+
@provider_params ||= {}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def name
|
|
68
|
+
klass_name = self.class.name
|
|
69
|
+
normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
|
|
70
|
+
normalized.encode('ASCII', replace: '')
|
|
71
|
+
.gsub(/[^a-zA-Z0-9_-]/, '-')
|
|
72
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
73
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
74
|
+
.downcase
|
|
75
|
+
.delete_suffix('_tool')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def description
|
|
79
|
+
self.class.description
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parameters
|
|
83
|
+
self.class.parameters
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def provider_params
|
|
87
|
+
self.class.provider_params
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def params_schema
|
|
91
|
+
return @params_schema if defined?(@params_schema)
|
|
92
|
+
|
|
93
|
+
@params_schema = begin
|
|
94
|
+
definition = self.class.params_schema_definition
|
|
95
|
+
if definition&.present?
|
|
96
|
+
definition.json_schema
|
|
97
|
+
elsif parameters.any?
|
|
98
|
+
SchemaDefinition.from_parameters(parameters)&.json_schema
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def call(args)
|
|
104
|
+
normalized_args = normalize_args(args)
|
|
105
|
+
validation_error = validate_keyword_arguments(normalized_args)
|
|
106
|
+
return { error: "Invalid tool arguments: #{validation_error}" } if validation_error
|
|
107
|
+
|
|
108
|
+
Legion::Extensions::Llm.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
|
|
109
|
+
result = execute(**normalized_args)
|
|
110
|
+
Legion::Extensions::Llm.logger.debug { "Tool #{name} returned: #{result.inspect}" }
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def execute(...)
|
|
115
|
+
raise NotImplementedError, 'Subclasses must implement #execute'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
protected
|
|
119
|
+
|
|
120
|
+
def halt(message)
|
|
121
|
+
Halt.new(message)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def normalize_args(args)
|
|
125
|
+
return {} if args.nil?
|
|
126
|
+
return args.transform_keys(&:to_sym) if args.respond_to?(:transform_keys)
|
|
127
|
+
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def validate_keyword_arguments(arguments)
|
|
132
|
+
required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
|
|
133
|
+
|
|
134
|
+
return nil if required_keywords.empty? && optional_keywords.empty?
|
|
135
|
+
|
|
136
|
+
argument_keys = arguments.keys
|
|
137
|
+
missing_keyword = first_missing_keyword(required_keywords, argument_keys)
|
|
138
|
+
return "missing keyword: #{missing_keyword}" if missing_keyword
|
|
139
|
+
return nil if accepts_extra_keywords
|
|
140
|
+
|
|
141
|
+
allowed_keywords = required_keywords + optional_keywords
|
|
142
|
+
unknown_keyword = first_unknown_keyword(argument_keys, allowed_keywords)
|
|
143
|
+
return "unknown keyword: #{unknown_keyword}" if unknown_keyword
|
|
144
|
+
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def execute_keyword_signature
|
|
149
|
+
keyword_signature = method(:execute).parameters
|
|
150
|
+
required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
|
|
151
|
+
optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
|
|
152
|
+
accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
|
|
153
|
+
|
|
154
|
+
[required_keywords, optional_keywords, accepts_extra_keywords]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def first_missing_keyword(required_keywords, argument_keys)
|
|
158
|
+
(required_keywords - argument_keys).first
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def first_unknown_keyword(argument_keys, allowed_keywords)
|
|
162
|
+
(argument_keys - allowed_keywords).first
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Wraps schema handling for tool parameters, supporting JSON Schema hashes,
|
|
166
|
+
# Legion::Extensions::Llm::Schema instances/classes, and DSL blocks.
|
|
167
|
+
class SchemaDefinition
|
|
168
|
+
def self.from_parameters(parameters)
|
|
169
|
+
return nil if parameters.nil? || parameters.empty?
|
|
170
|
+
|
|
171
|
+
properties = parameters.to_h do |name, param|
|
|
172
|
+
schema = {
|
|
173
|
+
type: map_type(param.type),
|
|
174
|
+
description: param.description
|
|
175
|
+
}.compact
|
|
176
|
+
|
|
177
|
+
schema[:items] = default_items_schema if schema[:type] == 'array'
|
|
178
|
+
|
|
179
|
+
[name.to_s, schema]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
required = parameters.select { |_, param| param.required }.keys.map(&:to_s)
|
|
183
|
+
|
|
184
|
+
json_schema = {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: properties,
|
|
187
|
+
required: required,
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
strict: true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
new(schema: json_schema)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.map_type(type)
|
|
196
|
+
case type.to_s
|
|
197
|
+
when 'integer', 'int' then 'integer'
|
|
198
|
+
when 'number', 'float', 'double' then 'number'
|
|
199
|
+
when 'boolean' then 'boolean'
|
|
200
|
+
when 'array' then 'array'
|
|
201
|
+
when 'object' then 'object'
|
|
202
|
+
else
|
|
203
|
+
'string'
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.default_items_schema
|
|
208
|
+
{ type: 'string' }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def initialize(schema: nil, block: nil)
|
|
212
|
+
@schema = schema
|
|
213
|
+
@block = block
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def present?
|
|
217
|
+
@schema || @block
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def json_schema
|
|
221
|
+
@json_schema ||= Legion::Extensions::Llm::Utils.deep_stringify_keys(resolve_schema)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def resolve_schema
|
|
227
|
+
return resolve_direct_schema(@schema) if @schema
|
|
228
|
+
return build_from_block(&@block) if @block
|
|
229
|
+
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def resolve_direct_schema(schema)
|
|
234
|
+
return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema)
|
|
235
|
+
return Legion::Extensions::Llm::Utils.deep_dup(schema) if schema.is_a?(Hash)
|
|
236
|
+
if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
|
|
237
|
+
return extract_schema(schema.new.to_json_schema)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def build_from_block(&)
|
|
244
|
+
schema_class = Legion::Extensions::Llm::Schema.create(&)
|
|
245
|
+
extract_schema(schema_class.new.to_json_schema)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def extract_schema(schema_hash)
|
|
249
|
+
return nil unless schema_hash.is_a?(Hash)
|
|
250
|
+
|
|
251
|
+
schema = schema_hash[:schema] || schema_hash['schema'] || schema_hash
|
|
252
|
+
Legion::Extensions::Llm::Utils.deep_dup(schema)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Represents a function call from an AI model to a Tool.
|
|
7
|
+
class ToolCall
|
|
8
|
+
attr_reader :id, :name, :arguments
|
|
9
|
+
attr_accessor :thought_signature
|
|
10
|
+
|
|
11
|
+
def initialize(id:, name:, arguments: {}, thought_signature: nil)
|
|
12
|
+
@id = id
|
|
13
|
+
@name = name
|
|
14
|
+
@arguments = arguments
|
|
15
|
+
@thought_signature = thought_signature
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
id: @id,
|
|
21
|
+
name: @name,
|
|
22
|
+
arguments: @arguments,
|
|
23
|
+
thought_signature: @thought_signature
|
|
24
|
+
}.compact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Represents a transcription of audio content.
|
|
7
|
+
class Transcription
|
|
8
|
+
attr_reader :text, :model, :language, :duration, :segments, :input_tokens, :output_tokens
|
|
9
|
+
|
|
10
|
+
def initialize(text:, model:, **attributes)
|
|
11
|
+
@text = text
|
|
12
|
+
@model = model
|
|
13
|
+
@language = attributes[:language]
|
|
14
|
+
@duration = attributes[:duration]
|
|
15
|
+
@segments = attributes[:segments]
|
|
16
|
+
@input_tokens = attributes[:input_tokens]
|
|
17
|
+
@output_tokens = attributes[:output_tokens]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.transcribe(audio_file, **kwargs)
|
|
21
|
+
model = kwargs.delete(:model)
|
|
22
|
+
language = kwargs.delete(:language)
|
|
23
|
+
provider = kwargs.delete(:provider)
|
|
24
|
+
assume_model_exists = kwargs.delete(:assume_model_exists) { false }
|
|
25
|
+
context = kwargs.delete(:context)
|
|
26
|
+
options = kwargs
|
|
27
|
+
|
|
28
|
+
config = context&.config || Legion::Extensions::Llm.config
|
|
29
|
+
model ||= config.default_transcription_model
|
|
30
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
31
|
+
config: config)
|
|
32
|
+
model_id = model.id
|
|
33
|
+
|
|
34
|
+
provider_instance.transcribe(audio_file, model: model_id, language:, **options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Provides utility functions for data manipulation within the Legion::Extensions::Llm library
|
|
7
|
+
module Utils
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def hash_get(hash, key)
|
|
11
|
+
hash[key.to_sym] || hash[key.to_s]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_safe_array(item)
|
|
15
|
+
case item
|
|
16
|
+
when Array
|
|
17
|
+
item
|
|
18
|
+
when Hash
|
|
19
|
+
[item]
|
|
20
|
+
else
|
|
21
|
+
Array(item)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_time(value)
|
|
26
|
+
return unless value
|
|
27
|
+
|
|
28
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_date(value)
|
|
32
|
+
return unless value
|
|
33
|
+
|
|
34
|
+
value.is_a?(Date) ? value : Date.parse(value.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deep_merge(original, overrides)
|
|
38
|
+
original.merge(overrides) do |_key, original_value, overrides_value|
|
|
39
|
+
if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
|
|
40
|
+
deep_merge(original_value, overrides_value)
|
|
41
|
+
else
|
|
42
|
+
overrides_value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deep_dup(value)
|
|
48
|
+
case value
|
|
49
|
+
when Hash
|
|
50
|
+
value.each_with_object({}) do |(key, val), duped|
|
|
51
|
+
duped[deep_dup(key)] = deep_dup(val)
|
|
52
|
+
end
|
|
53
|
+
when Array
|
|
54
|
+
value.map { |item| deep_dup(item) }
|
|
55
|
+
else
|
|
56
|
+
begin
|
|
57
|
+
value.dup
|
|
58
|
+
rescue TypeError
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def deep_stringify_keys(value)
|
|
65
|
+
case value
|
|
66
|
+
when Hash
|
|
67
|
+
value.each_with_object({}) do |(key, val), result|
|
|
68
|
+
result[key.to_s] = deep_stringify_keys(val)
|
|
69
|
+
end
|
|
70
|
+
when Array
|
|
71
|
+
value.map { |item| deep_stringify_keys(item) }
|
|
72
|
+
when Symbol
|
|
73
|
+
value.to_s
|
|
74
|
+
else
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def deep_symbolize_keys(value)
|
|
80
|
+
case value
|
|
81
|
+
when Hash
|
|
82
|
+
value.each_with_object({}) do |(key, val), result|
|
|
83
|
+
symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
|
84
|
+
result[symbolized_key] = deep_symbolize_keys(val)
|
|
85
|
+
end
|
|
86
|
+
when Array
|
|
87
|
+
value.map { |item| deep_symbolize_keys(item) }
|
|
88
|
+
else
|
|
89
|
+
value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -1,23 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require '
|
|
5
|
-
require '
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'digest/sha1'
|
|
6
|
+
require 'event_stream_parser'
|
|
7
|
+
require 'faraday'
|
|
8
|
+
require 'faraday/multipart'
|
|
9
|
+
require 'faraday/retry'
|
|
10
|
+
require 'legion/json'
|
|
11
|
+
require 'logger'
|
|
12
|
+
require 'marcel'
|
|
13
|
+
require 'ruby_llm/schema'
|
|
14
|
+
require 'securerandom'
|
|
15
|
+
require 'time'
|
|
16
|
+
require 'zeitwerk'
|
|
17
|
+
require_relative 'llm/version'
|
|
6
18
|
|
|
7
19
|
module Legion
|
|
8
20
|
module Extensions
|
|
9
21
|
# Legion-native namespace for the shared LLM provider framework.
|
|
10
22
|
module Llm
|
|
11
|
-
|
|
23
|
+
loader = Zeitwerk::Loader.new
|
|
24
|
+
loader.tag = 'lex-llm'
|
|
25
|
+
loader.inflector.inflect(
|
|
26
|
+
'api' => 'API',
|
|
27
|
+
'llm' => 'Llm',
|
|
28
|
+
'open_ai_compatible' => 'OpenAICompatible',
|
|
29
|
+
'pdf' => 'PDF',
|
|
30
|
+
'ui' => 'UI'
|
|
31
|
+
)
|
|
32
|
+
loader.ignore("#{__dir__}/llm/version.rb")
|
|
33
|
+
loader.push_dir("#{__dir__}/llm", namespace: self)
|
|
34
|
+
loader.setup
|
|
35
|
+
|
|
36
|
+
Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
|
|
12
37
|
|
|
13
38
|
# Provider-neutral value objects exposed under the Legion extension namespace.
|
|
14
39
|
module Types
|
|
15
|
-
ModelOffering =
|
|
40
|
+
ModelOffering = Routing::ModelOffering unless const_defined?(:ModelOffering, false)
|
|
16
41
|
end
|
|
17
42
|
|
|
18
43
|
# Shared routing helpers exposed under the Legion extension namespace.
|
|
19
44
|
module Routing
|
|
20
|
-
LaneKey =
|
|
45
|
+
LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def context
|
|
50
|
+
context_config = config.dup
|
|
51
|
+
yield context_config if block_given?
|
|
52
|
+
Context.new(context_config)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def chat(...)
|
|
56
|
+
Chat.new(...)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def embed(...)
|
|
60
|
+
Embedding.embed(...)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def moderate(...)
|
|
64
|
+
Moderation.moderate(...)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def paint(...)
|
|
68
|
+
Image.paint(...)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def transcribe(...)
|
|
72
|
+
Transcription.transcribe(...)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def models
|
|
76
|
+
Models.instance
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def providers
|
|
80
|
+
Provider.providers.values
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def configure
|
|
84
|
+
yield config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def config
|
|
88
|
+
@config ||= Configuration.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def logger
|
|
92
|
+
@logger ||= config.logger || Logger.new(
|
|
93
|
+
config.log_file,
|
|
94
|
+
progname: 'Legion::Extensions::Llm',
|
|
95
|
+
level: config.log_level
|
|
96
|
+
)
|
|
97
|
+
end
|
|
21
98
|
end
|
|
22
99
|
|
|
23
100
|
def self.default_settings
|
|
@@ -45,6 +122,8 @@ module Legion
|
|
|
45
122
|
def self.provider_settings(...)
|
|
46
123
|
ProviderSettings.build(...)
|
|
47
124
|
end
|
|
125
|
+
|
|
126
|
+
loader.eager_load
|
|
48
127
|
end
|
|
49
128
|
end
|
|
50
129
|
end
|