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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +69 -26
- data/lib/llm/context.rb +15 -10
- data/lib/llm/eventstream/parser.rb +40 -8
- data/lib/llm/providers/anthropic/stream_parser.rb +6 -3
- data/lib/llm/providers/google/stream_parser.rb +6 -3
- data/lib/llm/providers/ollama/stream_parser.rb +3 -2
- data/lib/llm/providers/openai/responses/stream_parser.rb +216 -91
- data/lib/llm/providers/openai/stream_parser.rb +111 -57
- data/lib/llm/response.rb +12 -4
- data/lib/llm/sequel/plugin.rb +252 -0
- data/lib/llm/stream/queue.rb +2 -2
- data/lib/llm/stream.rb +2 -2
- data/lib/llm/version.rb +1 -1
- data/lib/sequel/plugins/llm.rb +8 -0
- metadata +3 -1
|
@@ -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
|
data/lib/llm/stream/queue.rb
CHANGED
|
@@ -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 |
|
|
57
|
+
results.each_with_index do |result, idx|
|
|
58
58
|
tool = tasks[idx]&.function
|
|
59
|
-
@stream.on_tool_return(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]
|
|
89
|
+
# @param [LLM::Function::Return] result
|
|
90
90
|
# The completed tool return.
|
|
91
91
|
# @return [nil]
|
|
92
|
-
def on_tool_return(tool,
|
|
92
|
+
def on_tool_return(tool, result)
|
|
93
93
|
nil
|
|
94
94
|
end
|
|
95
95
|
|
data/lib/llm/version.rb
CHANGED
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.
|
|
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:
|