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,15 @@
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
@@ -0,0 +1,365 @@
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