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.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +12 -1
  4. data/Gemfile +1 -19
  5. data/README.md +25 -26
  6. data/lex-llm.gemspec +2 -2
  7. data/lib/legion/extensions/llm/agent.rb +366 -0
  8. data/lib/legion/extensions/llm/aliases.rb +42 -0
  9. data/lib/legion/extensions/llm/attachment.rb +229 -0
  10. data/lib/legion/extensions/llm/chat.rb +355 -0
  11. data/lib/legion/extensions/llm/chunk.rb +10 -0
  12. data/lib/legion/extensions/llm/configuration.rb +82 -0
  13. data/lib/legion/extensions/llm/connection.rb +134 -0
  14. data/lib/legion/extensions/llm/content.rb +81 -0
  15. data/lib/legion/extensions/llm/context.rb +33 -0
  16. data/lib/legion/extensions/llm/embedding.rb +33 -0
  17. data/lib/legion/extensions/llm/error.rb +116 -0
  18. data/lib/legion/extensions/llm/image.rb +109 -0
  19. data/lib/legion/extensions/llm/message.rb +111 -0
  20. data/lib/legion/extensions/llm/mime_type.rb +75 -0
  21. data/lib/legion/extensions/llm/model/info.rb +117 -0
  22. data/lib/legion/extensions/llm/model/modalities.rb +26 -0
  23. data/lib/legion/extensions/llm/model/pricing.rb +52 -0
  24. data/lib/legion/extensions/llm/model/pricing_category.rb +50 -0
  25. data/lib/legion/extensions/llm/model/pricing_tier.rb +37 -0
  26. data/lib/legion/extensions/llm/model.rb +11 -0
  27. data/lib/legion/extensions/llm/models.rb +514 -0
  28. data/lib/{lex_llm → legion/extensions/llm}/models_schema.json +1 -1
  29. data/lib/legion/extensions/llm/moderation.rb +60 -0
  30. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +276 -0
  31. data/lib/legion/extensions/llm/provider.rb +337 -0
  32. data/lib/legion/extensions/llm/routing/lane_key.rb +57 -0
  33. data/lib/legion/extensions/llm/routing/model_offering.rb +173 -0
  34. data/lib/legion/extensions/llm/routing.rb +11 -0
  35. data/lib/legion/extensions/llm/stream_accumulator.rb +209 -0
  36. data/lib/legion/extensions/llm/streaming.rb +181 -0
  37. data/lib/legion/extensions/llm/thinking.rb +53 -0
  38. data/lib/legion/extensions/llm/tokens.rb +51 -0
  39. data/lib/legion/extensions/llm/tool.rb +258 -0
  40. data/lib/legion/extensions/llm/tool_call.rb +29 -0
  41. data/lib/legion/extensions/llm/transcription.rb +39 -0
  42. data/lib/legion/extensions/llm/utils.rb +95 -0
  43. data/lib/legion/extensions/llm/version.rb +9 -0
  44. data/lib/legion/extensions/llm.rb +85 -6
  45. metadata +40 -122
  46. data/lib/generators/lex_llm/agent/agent_generator.rb +0 -36
  47. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +0 -6
  48. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  49. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +0 -256
  50. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +0 -38
  51. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +0 -21
  52. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +0 -14
  53. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  54. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +0 -12
  55. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +0 -16
  56. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +0 -31
  57. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +0 -31
  58. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +0 -9
  59. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +0 -27
  60. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +0 -14
  61. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +0 -1
  62. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +0 -13
  63. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +0 -23
  64. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +0 -10
  65. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +0 -2
  66. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +0 -4
  67. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +0 -14
  68. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +0 -13
  69. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +0 -21
  70. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +0 -17
  71. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +0 -40
  72. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +0 -27
  73. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +0 -16
  74. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +0 -29
  75. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +0 -28
  76. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +0 -11
  77. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +0 -25
  78. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +0 -9
  79. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +0 -1
  80. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +0 -8
  81. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +0 -21
  82. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +0 -6
  83. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +0 -2
  84. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +0 -4
  85. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +0 -9
  86. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +0 -7
  87. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +0 -8
  88. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +0 -16
  89. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +0 -15
  90. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +0 -38
  91. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +0 -17
  92. data/lib/generators/lex_llm/generator_helpers.rb +0 -214
  93. data/lib/generators/lex_llm/install/install_generator.rb +0 -109
  94. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  95. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +0 -3
  96. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +0 -7
  97. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +0 -19
  98. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +0 -39
  99. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  100. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +0 -20
  101. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +0 -4
  102. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +0 -3
  103. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +0 -3
  104. data/lib/generators/lex_llm/schema/schema_generator.rb +0 -26
  105. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +0 -2
  106. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +0 -9
  107. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +0 -13
  108. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +0 -13
  109. data/lib/generators/lex_llm/tool/tool_generator.rb +0 -96
  110. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  111. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  112. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  113. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  114. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  115. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  116. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  117. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  118. data/lib/lex_llm/active_record/acts_as.rb +0 -180
  119. data/lib/lex_llm/active_record/acts_as_legacy.rb +0 -503
  120. data/lib/lex_llm/active_record/chat_methods.rb +0 -468
  121. data/lib/lex_llm/active_record/message_methods.rb +0 -131
  122. data/lib/lex_llm/active_record/model_methods.rb +0 -76
  123. data/lib/lex_llm/active_record/payload_helpers.rb +0 -26
  124. data/lib/lex_llm/active_record/tool_call_methods.rb +0 -15
  125. data/lib/lex_llm/agent.rb +0 -365
  126. data/lib/lex_llm/aliases.rb +0 -38
  127. data/lib/lex_llm/attachment.rb +0 -223
  128. data/lib/lex_llm/chat.rb +0 -351
  129. data/lib/lex_llm/chunk.rb +0 -6
  130. data/lib/lex_llm/configuration.rb +0 -81
  131. data/lib/lex_llm/connection.rb +0 -130
  132. data/lib/lex_llm/content.rb +0 -77
  133. data/lib/lex_llm/context.rb +0 -29
  134. data/lib/lex_llm/embedding.rb +0 -29
  135. data/lib/lex_llm/error.rb +0 -112
  136. data/lib/lex_llm/image.rb +0 -105
  137. data/lib/lex_llm/message.rb +0 -107
  138. data/lib/lex_llm/mime_type.rb +0 -71
  139. data/lib/lex_llm/model/info.rb +0 -113
  140. data/lib/lex_llm/model/modalities.rb +0 -22
  141. data/lib/lex_llm/model/pricing.rb +0 -48
  142. data/lib/lex_llm/model/pricing_category.rb +0 -46
  143. data/lib/lex_llm/model/pricing_tier.rb +0 -33
  144. data/lib/lex_llm/model.rb +0 -7
  145. data/lib/lex_llm/models.rb +0 -506
  146. data/lib/lex_llm/moderation.rb +0 -56
  147. data/lib/lex_llm/provider/open_ai_compatible.rb +0 -219
  148. data/lib/lex_llm/provider.rb +0 -278
  149. data/lib/lex_llm/railtie.rb +0 -35
  150. data/lib/lex_llm/routing/lane_key.rb +0 -51
  151. data/lib/lex_llm/routing/model_offering.rb +0 -169
  152. data/lib/lex_llm/routing.rb +0 -7
  153. data/lib/lex_llm/stream_accumulator.rb +0 -203
  154. data/lib/lex_llm/streaming.rb +0 -175
  155. data/lib/lex_llm/thinking.rb +0 -49
  156. data/lib/lex_llm/tokens.rb +0 -47
  157. data/lib/lex_llm/tool.rb +0 -254
  158. data/lib/lex_llm/tool_call.rb +0 -25
  159. data/lib/lex_llm/transcription.rb +0 -35
  160. data/lib/lex_llm/utils.rb +0 -91
  161. data/lib/lex_llm/version.rb +0 -5
  162. data/lib/lex_llm.rb +0 -96
  163. data/lib/tasks/lex_llm.rake +0 -23
  164. /data/lib/{lex_llm → legion/extensions/llm}/aliases.json +0 -0
  165. /data/lib/{lex_llm → legion/extensions/llm}/models.json +0 -0
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- module ActiveRecord
5
- # Methods mixed into tool call models.
6
- module ToolCallMethods
7
- extend ActiveSupport::Concern
8
- include PayloadHelpers
9
-
10
- def tool_error_message
11
- payload_error_message(arguments)
12
- end
13
- end
14
- end
15
- end
data/lib/lex_llm/agent.rb DELETED
@@ -1,365 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'erb'
4
- require 'forwardable'
5
- require 'pathname'
6
- require 'ruby_llm/schema'
7
-
8
- module LexLLM
9
- # Base class for simple, class-configured agents.
10
- class Agent
11
- extend Forwardable
12
- include Enumerable
13
-
14
- class << self
15
- def inherited(subclass)
16
- super
17
- subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
18
- subclass.instance_variable_set(:@tools, (@tools || []).dup)
19
- subclass.instance_variable_set(:@instructions, @instructions)
20
- subclass.instance_variable_set(:@temperature, @temperature)
21
- subclass.instance_variable_set(:@thinking, @thinking)
22
- subclass.instance_variable_set(:@params, (@params || {}).dup)
23
- subclass.instance_variable_set(:@headers, (@headers || {}).dup)
24
- subclass.instance_variable_set(:@schema, @schema)
25
- subclass.instance_variable_set(:@context, @context)
26
- subclass.instance_variable_set(:@chat_model, @chat_model)
27
- subclass.instance_variable_set(:@input_names, (@input_names || []).dup)
28
- end
29
-
30
- def model(model_id = nil, **options)
31
- options[:model] = model_id unless model_id.nil?
32
- @chat_kwargs = options
33
- end
34
-
35
- def tools(*tools, &block)
36
- return @tools || [] if tools.empty? && !block_given?
37
-
38
- @tools = block_given? ? block : tools.flatten
39
- end
40
-
41
- def instructions(text = nil, **prompt_locals, &block)
42
- if text.nil? && prompt_locals.empty? && !block_given?
43
- @instructions ||= { prompt: 'instructions', locals: {} }
44
- return @instructions
45
- end
46
-
47
- @instructions = block || text || { prompt: 'instructions', locals: prompt_locals }
48
- end
49
-
50
- def temperature(value = nil)
51
- return @temperature if value.nil?
52
-
53
- @temperature = value
54
- end
55
-
56
- def thinking(effort: nil, budget: nil)
57
- return @thinking if effort.nil? && budget.nil?
58
-
59
- @thinking = { effort: effort, budget: budget }
60
- end
61
-
62
- def params(**params, &block)
63
- return @params || {} if params.empty? && !block_given?
64
-
65
- @params = block_given? ? block : params
66
- end
67
-
68
- def headers(**headers, &block)
69
- return @headers || {} if headers.empty? && !block_given?
70
-
71
- @headers = block_given? ? block : headers
72
- end
73
-
74
- def schema(value = nil, &block)
75
- return @schema if value.nil? && !block_given?
76
-
77
- @schema = block_given? ? block : value
78
- end
79
-
80
- def context(value = nil)
81
- return @context if value.nil?
82
-
83
- @context = value
84
- end
85
-
86
- def chat_model(value = nil)
87
- return @chat_model if value.nil?
88
-
89
- @chat_model = value
90
- remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
91
- end
92
-
93
- def inputs(*names)
94
- return @input_names || [] if names.empty?
95
-
96
- @input_names = names.flatten.map(&:to_sym)
97
- end
98
-
99
- def chat_kwargs
100
- @chat_kwargs || {}
101
- end
102
-
103
- def chat(**kwargs)
104
- input_values, chat_options = partition_inputs(kwargs)
105
- chat = LexLLM.chat(**chat_kwargs, **chat_options)
106
- apply_configuration(chat, input_values:, persist_instructions: true)
107
- chat
108
- end
109
-
110
- def create(**)
111
- with_rails_chat_record(:create, **)
112
- end
113
-
114
- def create!(**)
115
- with_rails_chat_record(:create!, **)
116
- end
117
-
118
- def find(id, **kwargs)
119
- raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model
120
-
121
- input_values, = partition_inputs(kwargs)
122
- record = resolved_chat_model.find(id)
123
- apply_configuration(record, input_values:, persist_instructions: false)
124
-
125
- record
126
- end
127
-
128
- def sync_instructions!(chat_or_id, **kwargs)
129
- raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
130
-
131
- input_values, = partition_inputs(kwargs)
132
- record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
133
- apply_assume_model_exists(record)
134
- runtime = runtime_context(chat: record, inputs: input_values)
135
- instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
136
- return record if instructions_value.nil?
137
-
138
- record.with_instructions(instructions_value)
139
- record
140
- end
141
-
142
- def render_prompt(name, chat:, inputs:, locals:)
143
- path = prompt_path_for(name)
144
- unless File.exist?(path)
145
- raise LexLLM::PromptNotFoundError,
146
- "Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
147
- end
148
-
149
- resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
150
- ERB.new(File.read(path)).result_with_hash(resolved_locals)
151
- end
152
-
153
- private
154
-
155
- def with_rails_chat_record(method_name, **kwargs)
156
- raise ArgumentError, 'chat_model must be configured to use create/create!' unless resolved_chat_model
157
-
158
- input_values, chat_options = partition_inputs(kwargs)
159
- record = resolved_chat_model.public_send(method_name, **chat_kwargs, **chat_options)
160
- apply_configuration(record, input_values:, persist_instructions: true) if record
161
- record
162
- end
163
-
164
- def apply_configuration(chat_object, input_values:, persist_instructions:)
165
- runtime = runtime_context(chat: chat_object, inputs: input_values)
166
- llm_chat = llm_chat_for(chat_object)
167
-
168
- apply_context(llm_chat)
169
- apply_instructions(chat_object, runtime, inputs: input_values, persist: persist_instructions)
170
- apply_tools(llm_chat, runtime)
171
- apply_temperature(llm_chat)
172
- apply_thinking(llm_chat)
173
- apply_params(llm_chat, runtime)
174
- apply_headers(llm_chat, runtime)
175
- apply_schema(llm_chat, runtime)
176
- end
177
-
178
- def apply_context(llm_chat)
179
- llm_chat.with_context(context) if context
180
- end
181
-
182
- def apply_instructions(chat_object, runtime, inputs:, persist:)
183
- value = resolved_instructions_value(chat_object, runtime, inputs:)
184
- return if value.nil?
185
-
186
- target = instruction_target(chat_object, persist:)
187
- return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
188
-
189
- target.with_instructions(value)
190
- end
191
-
192
- def apply_tools(llm_chat, runtime)
193
- tools_to_apply = Array(evaluate(tools, runtime))
194
- llm_chat.with_tools(*tools_to_apply) unless tools_to_apply.empty?
195
- end
196
-
197
- def apply_temperature(llm_chat)
198
- llm_chat.with_temperature(temperature) unless temperature.nil?
199
- end
200
-
201
- def apply_thinking(llm_chat)
202
- llm_chat.with_thinking(**thinking) if thinking
203
- end
204
-
205
- def apply_params(llm_chat, runtime)
206
- value = evaluate(params, runtime)
207
- llm_chat.with_params(**value) if value && !value.empty?
208
- end
209
-
210
- def apply_headers(llm_chat, runtime)
211
- value = evaluate(headers, runtime)
212
- llm_chat.with_headers(**value) if value && !value.empty?
213
- end
214
-
215
- def apply_schema(llm_chat, runtime)
216
- value = resolved_schema_value(runtime)
217
- llm_chat.with_schema(value) if value
218
- end
219
-
220
- def resolved_schema_value(runtime)
221
- value = schema
222
- return value unless value.is_a?(Proc)
223
-
224
- evaluate(value, runtime)
225
- rescue NoMethodError => e
226
- raise unless e.receiver.equal?(runtime)
227
-
228
- LexLLM::Schema.create(&value)
229
- end
230
-
231
- def llm_chat_for(chat_object)
232
- apply_assume_model_exists(chat_object)
233
- chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
234
- end
235
-
236
- def apply_assume_model_exists(chat_object)
237
- return unless chat_kwargs.key?(:assume_model_exists) &&
238
- resolved_chat_model &&
239
- chat_object.is_a?(resolved_chat_model)
240
-
241
- chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
242
- end
243
-
244
- def evaluate(value, runtime)
245
- value.is_a?(Proc) ? runtime.instance_exec(&value) : value
246
- end
247
-
248
- def resolved_instructions_value(chat_object, runtime, inputs:)
249
- value = evaluate(@instructions, runtime)
250
- return value unless prompt_instruction?(value)
251
-
252
- runtime.prompt(
253
- value[:prompt],
254
- **resolve_prompt_locals(value[:locals] || {}, runtime:, chat: chat_object, inputs:)
255
- )
256
- end
257
-
258
- def prompt_instruction?(value)
259
- value.is_a?(Hash) && value[:prompt]
260
- end
261
-
262
- def instruction_target(chat_object, persist:)
263
- if persist || !chat_object.respond_to?(:to_llm)
264
- chat_object
265
- else
266
- runtime_instruction_target(chat_object)
267
- end
268
- end
269
-
270
- def runtime_instruction_target(chat_object)
271
- return chat_object if chat_object.respond_to?(:with_runtime_instructions)
272
-
273
- chat_object.to_llm
274
- end
275
-
276
- def use_runtime_instructions?(target, persist:)
277
- !persist && target.respond_to?(:with_runtime_instructions)
278
- end
279
-
280
- def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
281
- base = { chat: chat }.merge(inputs)
282
- evaluated = locals.each_with_object({}) do |(key, value), acc|
283
- acc[key.to_sym] = value.is_a?(Proc) ? runtime.instance_exec(&value) : value
284
- end
285
- base.merge(evaluated)
286
- end
287
-
288
- def partition_inputs(kwargs)
289
- input_values = {}
290
- chat_options = {}
291
-
292
- kwargs.each do |key, value|
293
- symbolized_key = key.to_sym
294
- if inputs.include?(symbolized_key)
295
- input_values[symbolized_key] = value
296
- else
297
- chat_options[symbolized_key] = value
298
- end
299
- end
300
-
301
- [input_values, chat_options]
302
- end
303
-
304
- def runtime_context(chat:, inputs:)
305
- agent_class = self
306
- Object.new.tap do |runtime|
307
- runtime.define_singleton_method(:chat) { chat }
308
- runtime.define_singleton_method(:prompt) do |name, **locals|
309
- agent_class.render_prompt(name, chat:, inputs:, locals:)
310
- end
311
-
312
- inputs.each do |name, value|
313
- runtime.define_singleton_method(name) { value }
314
- end
315
- end
316
- end
317
-
318
- def prompt_path_for(name)
319
- filename = name.to_s
320
- filename += '.txt.erb' unless filename.end_with?('.txt.erb')
321
- prompt_root.join(prompt_agent_path, filename)
322
- end
323
-
324
- def prompt_agent_path
325
- class_name = name || 'agent'
326
- class_name.gsub('::', '/')
327
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
328
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
329
- .tr('-', '_')
330
- .downcase
331
- end
332
-
333
- def prompt_root
334
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
335
- Rails.root.join('app/prompts')
336
- else
337
- Pathname.new(Dir.pwd).join('app/prompts')
338
- end
339
- end
340
-
341
- def resolved_chat_model
342
- return @resolved_chat_model if defined?(@resolved_chat_model)
343
-
344
- @resolved_chat_model = case @chat_model
345
- when String then Object.const_get(@chat_model)
346
- else @chat_model
347
- end
348
- end
349
- end
350
-
351
- def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
352
- input_values, chat_options = self.class.send(:partition_inputs, kwargs)
353
- @chat = chat || LexLLM.chat(**self.class.chat_kwargs, **chat_options)
354
- self.class.send(:apply_configuration, @chat, input_values: input_values.merge(inputs || {}),
355
- persist_instructions:)
356
- end
357
-
358
- attr_reader :chat
359
-
360
- def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
361
- :with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
362
- :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
363
- :add_message, :reset_messages!
364
- end
365
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Manages model aliases for provider-specific versions
5
- class Aliases
6
- class << self
7
- def resolve(model_id, provider = nil)
8
- return model_id unless aliases[model_id]
9
-
10
- if provider
11
- aliases[model_id][provider.to_s] || model_id
12
- else
13
- aliases[model_id].values.first || model_id
14
- end
15
- end
16
-
17
- def aliases
18
- @aliases ||= load_aliases
19
- end
20
-
21
- def aliases_file
22
- File.expand_path('aliases.json', __dir__)
23
- end
24
-
25
- def load_aliases
26
- if File.exist?(aliases_file)
27
- Legion::JSON.parse(File.read(aliases_file), symbolize_names: false)
28
- else
29
- {}
30
- end
31
- end
32
-
33
- def reload!
34
- @aliases = load_aliases
35
- end
36
- end
37
- end
38
- end
@@ -1,223 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
- require 'uri'
5
-
6
- module LexLLM
7
- # A class representing a file attachment.
8
- class Attachment
9
- attr_reader :source, :filename, :mime_type
10
-
11
- def initialize(source, filename: nil)
12
- @source = source
13
- @source = source_type_cast
14
- @filename = filename || source_filename
15
-
16
- determine_mime_type
17
- end
18
-
19
- def url?
20
- @source.is_a?(URI) || (@source.is_a?(String) && @source.match?(%r{^https?://}))
21
- end
22
-
23
- def path?
24
- @source.is_a?(Pathname) || (@source.is_a?(String) && !url?)
25
- end
26
-
27
- def io_like?
28
- @source.respond_to?(:read) && !path? && !active_storage?
29
- end
30
-
31
- def active_storage?
32
- return false unless defined?(ActiveStorage)
33
-
34
- @source.is_a?(ActiveStorage::Blob) ||
35
- @source.is_a?(ActiveStorage::Attached::One) ||
36
- @source.is_a?(ActiveStorage::Attached::Many)
37
- end
38
-
39
- def content
40
- return @content if defined?(@content) && !@content.nil?
41
-
42
- if url?
43
- fetch_content
44
- elsif path?
45
- load_content_from_path
46
- elsif active_storage?
47
- load_content_from_active_storage
48
- elsif io_like?
49
- load_content_from_io
50
- else
51
- LexLLM.logger.warn "Source is neither a URL, path, ActiveStorage, nor IO-like: #{@source.class}"
52
- nil
53
- end
54
-
55
- @content
56
- end
57
-
58
- def encoded
59
- Base64.strict_encode64(content)
60
- end
61
-
62
- def save(path)
63
- return unless io_like?
64
-
65
- File.open(path, 'w') do |f|
66
- f.puts(@source.read)
67
- end
68
- end
69
-
70
- def for_llm
71
- case type
72
- when :text
73
- "<file name='#{filename}' mime_type='#{mime_type}'>#{content}</file>"
74
- else
75
- "data:#{mime_type};base64,#{encoded}"
76
- end
77
- end
78
-
79
- def type
80
- return :image if image?
81
- return :video if video?
82
- return :audio if audio?
83
- return :pdf if pdf?
84
- return :text if text?
85
-
86
- :unknown
87
- end
88
-
89
- def image?
90
- LexLLM::MimeType.image? mime_type
91
- end
92
-
93
- def video?
94
- LexLLM::MimeType.video? mime_type
95
- end
96
-
97
- def audio?
98
- LexLLM::MimeType.audio? mime_type
99
- end
100
-
101
- def format
102
- case mime_type
103
- when 'audio/mpeg'
104
- 'mp3'
105
- when 'audio/wav', 'audio/wave', 'audio/x-wav'
106
- 'wav'
107
- else
108
- mime_type.split('/').last
109
- end
110
- end
111
-
112
- def pdf?
113
- LexLLM::MimeType.pdf? mime_type
114
- end
115
-
116
- def text?
117
- LexLLM::MimeType.text? mime_type
118
- end
119
-
120
- def to_h
121
- { type: type, source: @source }
122
- end
123
-
124
- private
125
-
126
- def determine_mime_type
127
- return @mime_type = active_storage_content_type if active_storage? && active_storage_content_type.present?
128
-
129
- @mime_type = LexLLM::MimeType.for(url? ? nil : @source, name: @filename)
130
- @mime_type = LexLLM::MimeType.for(content) if @mime_type == 'application/octet-stream'
131
- @mime_type = 'audio/wav' if @mime_type == 'audio/x-wav' # Normalize WAV type
132
- end
133
-
134
- def fetch_content
135
- response = Connection.basic.get @source.to_s
136
- @content = response.body
137
- end
138
-
139
- def load_content_from_path
140
- @content = File.binread(@source)
141
- end
142
-
143
- def load_content_from_io
144
- @source.rewind if @source.respond_to? :rewind
145
- @content = @source.read
146
- end
147
-
148
- def load_content_from_active_storage
149
- return unless defined?(ActiveStorage)
150
-
151
- @content = case @source
152
- when ActiveStorage::Blob
153
- @source.download
154
- when ActiveStorage::Attached::One
155
- @source.blob&.download
156
- when ActiveStorage::Attached::Many
157
- # For multiple attachments, just take the first one
158
- # This maintains the single-attachment interface
159
- @source.blobs.first&.download
160
- end
161
- end
162
-
163
- def source_type_cast
164
- if url?
165
- URI(@source)
166
- elsif path?
167
- Pathname.new(@source)
168
- else
169
- @source
170
- end
171
- end
172
-
173
- def source_filename
174
- if url?
175
- File.basename(@source.path).to_s
176
- elsif path?
177
- @source.basename.to_s
178
- elsif io_like?
179
- extract_filename_from_io
180
- elsif active_storage?
181
- extract_filename_from_active_storage
182
- end
183
- end
184
-
185
- def extract_filename_from_io
186
- if defined?(ActionDispatch::Http::UploadedFile) && @source.is_a?(ActionDispatch::Http::UploadedFile)
187
- @source.original_filename.to_s
188
- elsif @source.respond_to?(:path)
189
- File.basename(@source.path).to_s
190
- else
191
- 'attachment'
192
- end
193
- end
194
-
195
- def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
196
- return 'attachment' unless defined?(ActiveStorage)
197
-
198
- case @source
199
- when ActiveStorage::Blob
200
- @source.filename.to_s
201
- when ActiveStorage::Attached::One
202
- @source.blob&.filename&.to_s || 'attachment'
203
- when ActiveStorage::Attached::Many
204
- @source.blobs.first&.filename&.to_s || 'attachment'
205
- else
206
- 'attachment'
207
- end
208
- end
209
-
210
- def active_storage_content_type
211
- return unless defined?(ActiveStorage)
212
-
213
- case @source
214
- when ActiveStorage::Blob
215
- @source.content_type
216
- when ActiveStorage::Attached::One
217
- @source.blob&.content_type
218
- when ActiveStorage::Attached::Many
219
- @source.blobs.first&.content_type
220
- end
221
- end
222
- end
223
- end