llm.rb 4.14.0 → 4.15.0

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.
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Sequel
4
+ ##
5
+ # Sequel plugin for persisting {LLM::Context LLM::Context} state.
6
+ #
7
+ # This plugin maps model columns onto provider selection, model
8
+ # selection, usage accounting, and serialized context data while
9
+ # leaving application-specific concerns such as credentials,
10
+ # associations, and UI shaping to the host app.
11
+ #
12
+ # Context state can be stored as a JSON string (`format: :string`, the
13
+ # default) or as a structured object (`format: :json` / `:jsonb`) for
14
+ # databases such as PostgreSQL that can persist JSON natively.
15
+ # `:json` and `:jsonb` expect a real JSON column type with Sequel handling
16
+ # JSON typecasting for the model.
17
+ module Plugin
18
+ EMPTY_HASH = {}.freeze
19
+ DEFAULT_USAGE_COLUMNS = {
20
+ input_tokens: :input_tokens,
21
+ output_tokens: :output_tokens,
22
+ total_tokens: :total_tokens
23
+ }.freeze
24
+ DEFAULTS = {
25
+ provider_column: :provider,
26
+ model_column: :model,
27
+ data_column: :data,
28
+ format: :string,
29
+ usage_columns: DEFAULT_USAGE_COLUMNS,
30
+ provider: EMPTY_HASH,
31
+ context: EMPTY_HASH
32
+ }.freeze
33
+
34
+ ##
35
+ # Called by Sequel when the plugin is first applied to a model class.
36
+ #
37
+ # This hook installs the plugin's class- and instance-level behavior on
38
+ # the target model. It runs before {configure}, so it should only attach
39
+ # methods and not depend on per-model plugin options.
40
+ #
41
+ # @param [Class] model
42
+ # @return [void]
43
+ def self.apply(model, **)
44
+ model.extend ClassMethods
45
+ model.include InstanceMethods
46
+ end
47
+
48
+ ##
49
+ # Called by Sequel after {apply} with the options passed to
50
+ # `plugin :llm, ...`.
51
+ #
52
+ # This hook merges plugin defaults with the model's explicit settings and
53
+ # stores the resolved configuration on the model class for later use by
54
+ # instance methods such as {InstanceMethods#llm} and {InstanceMethods#ctx}.
55
+ #
56
+ # @param [Class] model
57
+ # @param [Hash] options
58
+ # @option options [Symbol] :format
59
+ # Storage format for the serialized context. Use `:string` for text
60
+ # columns, or `:json` / `:jsonb` for structured JSON columns with Sequel
61
+ # JSON typecasting enabled.
62
+ # @return [void]
63
+ def self.configure(model, options = EMPTY_HASH)
64
+ options = DEFAULTS.merge(options)
65
+ usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
66
+ model.instance_variable_set(
67
+ :@llm_plugin_options,
68
+ options.merge(usage_columns: usage_columns.freeze).freeze
69
+ )
70
+ end
71
+ end
72
+
73
+ module Plugin::ClassMethods
74
+ ##
75
+ # @return [Hash]
76
+ def llm_plugin_options
77
+ @llm_plugin_options || DEFAULTS
78
+ end
79
+ end
80
+
81
+ module Plugin::InstanceMethods
82
+ ##
83
+ # Continues the stored context with new input and flushes it.
84
+ # @see LLM::Context#talk
85
+ # @return [LLM::Response]
86
+ def talk(...)
87
+ ctx.talk(...).tap { flush }
88
+ end
89
+
90
+ ##
91
+ # Continues the stored context through the Responses API and flushes it.
92
+ # @see LLM::Context#respond
93
+ # @return [LLM::Response]
94
+ def respond(...)
95
+ ctx.respond(...).tap { flush }
96
+ end
97
+
98
+ ##
99
+ # Waits for queued tool work to finish.
100
+ # @see LLM::Context#wait
101
+ # @return [Array<LLM::Function::Return>]
102
+ def wait(...)
103
+ ctx.wait(...)
104
+ end
105
+
106
+ ##
107
+ # Calls into the stored context.
108
+ # @see LLM::Context#call
109
+ # @return [Object]
110
+ def call(...)
111
+ ctx.call(...)
112
+ end
113
+
114
+ ##
115
+ # @see LLM::Context#messages
116
+ # @return [Array<LLM::Message>]
117
+ def messages
118
+ ctx.messages
119
+ end
120
+
121
+ ##
122
+ # @note The bang is used because Sequel reserves `model` for the
123
+ # underlying model class on instances.
124
+ # @see LLM::Context#model
125
+ # @return [String]
126
+ def model!
127
+ ctx.model
128
+ end
129
+
130
+ ##
131
+ # @see LLM::Context#functions
132
+ # @return [Array<LLM::Function>]
133
+ def functions
134
+ ctx.functions
135
+ end
136
+
137
+ ##
138
+ # @see LLM::Context#cost
139
+ # @return [LLM::Cost]
140
+ def cost
141
+ ctx.cost
142
+ end
143
+
144
+ ##
145
+ # @see LLM::Context#context_window
146
+ # @return [Integer]
147
+ def context_window
148
+ ctx.context_window
149
+ rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError
150
+ 0
151
+ end
152
+
153
+ ##
154
+ # Returns usage from the mapped usage columns.
155
+ # @return [LLM::Object]
156
+ def usage
157
+ LLM::Object.from(
158
+ input_tokens: self[columns[:input_tokens]] || 0,
159
+ output_tokens: self[columns[:output_tokens]] || 0,
160
+ total_tokens: self[columns[:total_tokens]] || 0
161
+ )
162
+ end
163
+
164
+ private
165
+
166
+ ##
167
+ # Returns the resolved provider instance for this record.
168
+ # @return [LLM::Provider]
169
+ def llm
170
+ options = self.class.llm_plugin_options
171
+ provider = self[columns[:provider_column]]
172
+ kwargs = resolve_options(options[:provider])
173
+ @llm ||= LLM.method(provider).call(**kwargs)
174
+ end
175
+
176
+ ##
177
+ # @return [LLM::Context]
178
+ def ctx
179
+ @ctx ||= begin
180
+ options = self.class.llm_plugin_options
181
+ params = resolve_options(options[:context]).dup
182
+ params[:model] ||= self[columns[:model_column]]
183
+ ctx = LLM::Context.new(llm, params.compact)
184
+ data = self[columns[:data_column]]
185
+ if data.nil? || data == ""
186
+ ctx
187
+ else
188
+ string = case options[:format]
189
+ when :string then data
190
+ when :json, :jsonb then LLM.json.dump(data)
191
+ else raise ArgumentError, "Unknown format: #{options[:format].inspect}"
192
+ end
193
+ ctx.restore(string:)
194
+ end
195
+ end
196
+ end
197
+
198
+ ##
199
+ # @return [void]
200
+ def flush
201
+ options = self.class.llm_plugin_options
202
+ update({
203
+ columns[:data_column] => serialize_context(options[:format]),
204
+ columns[:input_tokens] => ctx.usage.input_tokens,
205
+ columns[:output_tokens] => ctx.usage.output_tokens,
206
+ columns[:total_tokens] => ctx.usage.total_tokens
207
+ })
208
+ end
209
+
210
+ ##
211
+ # @return [Hash]
212
+ def resolve_option(option)
213
+ case option
214
+ when Proc then instance_exec(&option)
215
+ when Hash then option.dup
216
+ else option
217
+ end
218
+ end
219
+
220
+ ##
221
+ # @return [Hash]
222
+ def resolve_options(option)
223
+ case option
224
+ when Proc, Hash then resolve_option(option)
225
+ else EMPTY_HASH.dup
226
+ end
227
+ end
228
+
229
+ def serialize_context(format)
230
+ case format
231
+ when :string then ctx.to_json
232
+ when :json, :jsonb then ctx.to_h
233
+ else raise ArgumentError, "Unknown format: #{format.inspect}"
234
+ end
235
+ end
236
+
237
+ def columns
238
+ @columns ||= begin
239
+ options = self.class.llm_plugin_options
240
+ usage_columns = options[:usage_columns]
241
+ {
242
+ provider_column: options[:provider_column],
243
+ model_column: options[:model_column],
244
+ data_column: options[:data_column],
245
+ input_tokens: usage_columns[:input_tokens],
246
+ output_tokens: usage_columns[:output_tokens],
247
+ total_tokens: usage_columns[:total_tokens]
248
+ }.freeze
249
+ end
250
+ end
251
+ end
252
+ end
@@ -54,9 +54,9 @@ class LLM::Stream
54
54
  private
55
55
 
56
56
  def fire_hooks(tasks, results)
57
- results.each_with_index do |ret, idx|
57
+ results.each_with_index do |result, idx|
58
58
  tool = tasks[idx]&.function
59
- @stream.on_tool_return(tool, ret) if tool
59
+ @stream.on_tool_return(tool, result) if tool
60
60
  end
61
61
  results
62
62
  end
data/lib/llm/stream.rb CHANGED
@@ -86,10 +86,10 @@ module LLM
86
86
  # `tool.spawn(:fiber)`, or `tool.spawn(:task)`.
87
87
  # @param [LLM::Function] tool
88
88
  # The tool that returned.
89
- # @param [LLM::Function::Return] ret
89
+ # @param [LLM::Function::Return] result
90
90
  # The completed tool return.
91
91
  # @return [nil]
92
- def on_tool_return(tool, ret)
92
+ def on_tool_return(tool, result)
93
93
  nil
94
94
  end
95
95
 
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.14.0"
4
+ VERSION = "4.15.0"
5
5
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ require "llm/sequel/plugin"
6
+ Llm = LLM::Sequel::Plugin
7
+ end
8
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.14.0
4
+ version: 4.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -367,6 +367,7 @@ files:
367
367
  - lib/llm/schema/parser.rb
368
368
  - lib/llm/schema/string.rb
369
369
  - lib/llm/schema/version.rb
370
+ - lib/llm/sequel/plugin.rb
370
371
  - lib/llm/server_tool.rb
371
372
  - lib/llm/session.rb
372
373
  - lib/llm/stream.rb
@@ -381,6 +382,7 @@ files:
381
382
  - lib/llm/usage.rb
382
383
  - lib/llm/utils.rb
383
384
  - lib/llm/version.rb
385
+ - lib/sequel/plugins/llm.rb
384
386
  - llm.gemspec
385
387
  homepage: https://github.com/llmrb/llm.rb
386
388
  licenses: