llm.rb 5.4.0 → 6.1.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 +87 -1
- data/README.md +113 -25
- data/lib/llm/active_record/acts_as_agent.rb +5 -11
- data/lib/llm/active_record/acts_as_llm.rb +17 -37
- data/lib/llm/agent.rb +2 -0
- data/lib/llm/buffer.rb +8 -0
- data/lib/llm/compactor.rb +26 -7
- data/lib/llm/context/deserializer.rb +1 -0
- data/lib/llm/context.rb +48 -21
- data/lib/llm/error.rb +4 -0
- data/lib/llm/function/ractor/task.rb +8 -2
- data/lib/llm/function.rb +6 -2
- data/lib/llm/provider/transport/http/execution.rb +1 -1
- data/lib/llm/provider/transport/http/interruptible.rb +99 -94
- data/lib/llm/provider/transport/http.rb +3 -2
- data/lib/llm/provider.rb +8 -0
- data/lib/llm/sequel/agent.rb +2 -7
- data/lib/llm/sequel/plugin.rb +31 -38
- data/lib/llm/skill.rb +6 -0
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +1 -0
- metadata +15 -1
data/lib/llm/context.rb
CHANGED
|
@@ -40,6 +40,14 @@ module LLM
|
|
|
40
40
|
include Serializer
|
|
41
41
|
include Deserializer
|
|
42
42
|
|
|
43
|
+
ZERO_USAGE = LLM::Object.from(
|
|
44
|
+
input_tokens: 0,
|
|
45
|
+
output_tokens: 0,
|
|
46
|
+
reasoning_tokens: 0,
|
|
47
|
+
total_tokens: 0
|
|
48
|
+
)
|
|
49
|
+
private_constant :ZERO_USAGE
|
|
50
|
+
|
|
43
51
|
##
|
|
44
52
|
# Returns the accumulated message history for this context
|
|
45
53
|
# @return [LLM::Buffer<LLM::Message>]
|
|
@@ -104,6 +112,14 @@ module LLM
|
|
|
104
112
|
@compactor = compactor
|
|
105
113
|
end
|
|
106
114
|
|
|
115
|
+
##
|
|
116
|
+
# Returns whether the context has been compacted and no later model
|
|
117
|
+
# response has cleared that state.
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
# @api private
|
|
120
|
+
attr_accessor :compacted
|
|
121
|
+
alias_method :compacted?, :compacted
|
|
122
|
+
|
|
107
123
|
##
|
|
108
124
|
# Returns a guard, if configured.
|
|
109
125
|
#
|
|
@@ -172,13 +188,14 @@ module LLM
|
|
|
172
188
|
# puts res.messages[0].content
|
|
173
189
|
def talk(prompt, params = {})
|
|
174
190
|
return respond(prompt, params) if mode == :responses
|
|
175
|
-
@owner =
|
|
191
|
+
@owner = @llm.request_owner
|
|
176
192
|
compactor.compact!(prompt) if compactor.compact?(prompt)
|
|
177
193
|
params = params.merge(messages: @messages.to_a)
|
|
178
194
|
params = @params.merge(params)
|
|
179
195
|
prompt, params = transform(prompt, params)
|
|
180
196
|
bind!(params[:stream], params[:model], params[:tools])
|
|
181
197
|
res = @llm.complete(prompt, params)
|
|
198
|
+
self.compacted = false
|
|
182
199
|
role = params[:role] || @llm.user_role
|
|
183
200
|
role = @llm.tool_role if params[:role].nil? && [*prompt].grep(LLM::Function::Return).any?
|
|
184
201
|
@messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
|
|
@@ -201,7 +218,7 @@ module LLM
|
|
|
201
218
|
# res = ctx.respond("What is the capital of France?")
|
|
202
219
|
# puts res.output_text
|
|
203
220
|
def respond(prompt, params = {})
|
|
204
|
-
@owner =
|
|
221
|
+
@owner = @llm.request_owner
|
|
205
222
|
compactor.compact!(prompt) if compactor.compact?(prompt)
|
|
206
223
|
params = @params.merge(params)
|
|
207
224
|
prompt, params = transform(prompt, params)
|
|
@@ -209,6 +226,7 @@ module LLM
|
|
|
209
226
|
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
210
227
|
params = params.merge(previous_response_id: res_id, input: @messages.to_a).compact
|
|
211
228
|
res = @llm.responses.create(prompt, params)
|
|
229
|
+
self.compacted = false
|
|
212
230
|
role = params[:role] || @llm.user_role
|
|
213
231
|
@messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
|
|
214
232
|
@messages.concat [res.choices[-1]]
|
|
@@ -313,27 +331,31 @@ module LLM
|
|
|
313
331
|
# This is inspired by Go's context cancellation model.
|
|
314
332
|
# @return [nil]
|
|
315
333
|
def interrupt!
|
|
334
|
+
pending = functions.to_a
|
|
316
335
|
llm.interrupt!(@owner)
|
|
317
336
|
queue&.interrupt!
|
|
337
|
+
return if pending.empty?
|
|
338
|
+
pending.each(&:interrupt!)
|
|
339
|
+
returns = pending.map { _1.cancel(reason: "function call cancelled") }
|
|
340
|
+
@messages << LLM::Message.new(@llm.tool_role, returns)
|
|
341
|
+
nil
|
|
318
342
|
end
|
|
319
343
|
alias_method :cancel!, :interrupt!
|
|
320
344
|
|
|
321
345
|
##
|
|
322
346
|
# Returns token usage accumulated in this context
|
|
323
|
-
# @
|
|
324
|
-
# This method returns token usage for the latest
|
|
325
|
-
# assistant message, and it returns nil for non-assistant
|
|
326
|
-
# messages.
|
|
327
|
-
# @return [LLM::Object, nil]
|
|
347
|
+
# @return [LLM::Object]
|
|
328
348
|
def usage
|
|
329
|
-
usage = @messages.find(&:assistant?)&.usage
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
349
|
+
if usage = @messages.find(&:assistant?)&.usage
|
|
350
|
+
LLM::Object.from(
|
|
351
|
+
input_tokens: usage.input_tokens || 0,
|
|
352
|
+
output_tokens: usage.output_tokens || 0,
|
|
353
|
+
reasoning_tokens: usage.reasoning_tokens || 0,
|
|
354
|
+
total_tokens: usage.total_tokens || 0
|
|
355
|
+
)
|
|
356
|
+
else
|
|
357
|
+
ZERO_USAGE
|
|
358
|
+
end
|
|
337
359
|
end
|
|
338
360
|
|
|
339
361
|
##
|
|
@@ -403,7 +425,12 @@ module LLM
|
|
|
403
425
|
##
|
|
404
426
|
# @return [Hash]
|
|
405
427
|
def to_h
|
|
406
|
-
{
|
|
428
|
+
{
|
|
429
|
+
schema_version: 1,
|
|
430
|
+
model:,
|
|
431
|
+
compacted:,
|
|
432
|
+
messages: @messages.map { serialize_message(_1) }
|
|
433
|
+
}
|
|
407
434
|
end
|
|
408
435
|
|
|
409
436
|
##
|
|
@@ -432,12 +459,12 @@ module LLM
|
|
|
432
459
|
# Returns an _approximate_ cost for a given context
|
|
433
460
|
# based on both the provider, and model
|
|
434
461
|
def cost
|
|
435
|
-
return LLM::Cost.new(0, 0) unless usage
|
|
436
462
|
cost = LLM.registry_for(llm).cost(model:)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
463
|
+
input_cost = (cost.input.to_f / 1_000_000.0) * usage.input_tokens
|
|
464
|
+
output_cost = (cost.output.to_f / 1_000_000.0) * usage.output_tokens
|
|
465
|
+
LLM::Cost.new(input_cost, output_cost)
|
|
466
|
+
rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError
|
|
467
|
+
LLM::Cost.new(0, 0)
|
|
441
468
|
end
|
|
442
469
|
|
|
443
470
|
##
|
data/lib/llm/error.rb
CHANGED
|
@@ -63,6 +63,10 @@ module LLM
|
|
|
63
63
|
# When a request is interrupted
|
|
64
64
|
Interrupt = Class.new(Error)
|
|
65
65
|
|
|
66
|
+
##
|
|
67
|
+
# When a concurrency strategy cannot execute a given tool
|
|
68
|
+
RactorError = Class.new(Error)
|
|
69
|
+
|
|
66
70
|
##
|
|
67
71
|
# When a tool call cannot be mapped to a local tool
|
|
68
72
|
NoSuchToolError = Class.new(Error)
|
|
@@ -15,8 +15,12 @@ class LLM::Function
|
|
|
15
15
|
# @param [String, nil] id
|
|
16
16
|
# @param [String] name
|
|
17
17
|
# @param [Hash, Array, nil] arguments
|
|
18
|
+
# @param [LLM::Tracer, nil] tracer
|
|
19
|
+
# @param [Object, nil] span
|
|
18
20
|
# @return [LLM::Function::Ractor::Task]
|
|
19
|
-
def initialize(runner_class, id, name, arguments)
|
|
21
|
+
def initialize(runner_class, id, name, arguments, tracer: nil, span: nil)
|
|
22
|
+
@tracer = tracer
|
|
23
|
+
@span = span
|
|
20
24
|
@mailbox = Ractor::Mailbox.new(build_task(runner_class, id, name, arguments))
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -37,7 +41,9 @@ class LLM::Function
|
|
|
37
41
|
# @return [LLM::Function::Return]
|
|
38
42
|
def wait
|
|
39
43
|
id, name, value = mailbox.wait
|
|
40
|
-
Return.new(id, name, value)
|
|
44
|
+
result = Return.new(id, name, value)
|
|
45
|
+
@tracer&.on_tool_finish(result:, span: @span)
|
|
46
|
+
result
|
|
41
47
|
end
|
|
42
48
|
alias_method :value, :wait
|
|
43
49
|
|
data/lib/llm/function.rb
CHANGED
|
@@ -228,8 +228,12 @@ class LLM::Function
|
|
|
228
228
|
Fiber.yield
|
|
229
229
|
end.tap(&:resume)
|
|
230
230
|
when :ractor
|
|
231
|
-
raise
|
|
232
|
-
|
|
231
|
+
raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
|
|
232
|
+
if @runner.respond_to?(:skill?) && @runner.skill?
|
|
233
|
+
raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
|
|
234
|
+
end
|
|
235
|
+
span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
|
|
236
|
+
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:)
|
|
233
237
|
else
|
|
234
238
|
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
|
|
235
239
|
end
|
|
@@ -38,7 +38,7 @@ module LLM::Provider::Transport
|
|
|
38
38
|
perform_request(http, request, stream, stream_parser, &b)
|
|
39
39
|
end
|
|
40
40
|
[handle_response(res, tracer, span), span, tracer]
|
|
41
|
-
rescue *
|
|
41
|
+
rescue *transport.interrupt_errors
|
|
42
42
|
raise LLM::Interrupt, "request interrupted" if transport.interrupted?(owner)
|
|
43
43
|
raise
|
|
44
44
|
end
|
|
@@ -1,109 +1,114 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class LLM::Provider
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
17
|
-
Request = Struct.new(:http, :connection, keyword_init: true)
|
|
4
|
+
##
|
|
5
|
+
# Internal request interruption methods for
|
|
6
|
+
# {LLM::Provider::Transport::HTTP}.
|
|
7
|
+
#
|
|
8
|
+
# This module tracks active requests by execution owner and provides
|
|
9
|
+
# the logic used to interrupt an in-flight request by closing the
|
|
10
|
+
# active HTTP connection.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module Transport::HTTP::Interruptible
|
|
14
|
+
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
15
|
+
Request = Struct.new(:http, :connection, keyword_init: true)
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# The execution owner whose request should be interrupted
|
|
23
|
-
# @return [nil]
|
|
24
|
-
def interrupt!(owner)
|
|
25
|
-
req = request_for(owner) or return
|
|
26
|
-
lock { (@interrupts ||= {})[owner] = true }
|
|
27
|
-
if persistent_http?(req.http)
|
|
28
|
-
close_socket(req.connection&.http)
|
|
29
|
-
req.http.finish(req.connection)
|
|
30
|
-
elsif transient_http?(req.http)
|
|
31
|
-
close_socket(req.http)
|
|
32
|
-
req.http.finish if req.http.active?
|
|
33
|
-
end
|
|
34
|
-
rescue *INTERRUPT_ERRORS
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
17
|
+
def interrupt_errors
|
|
18
|
+
[*INTERRUPT_ERRORS, *optional_interrupt_errors]
|
|
19
|
+
end
|
|
39
20
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
21
|
+
##
|
|
22
|
+
# Interrupt an active request, if any.
|
|
23
|
+
# @param [Fiber] owner
|
|
24
|
+
# The execution owner whose request should be interrupted
|
|
25
|
+
# @return [nil]
|
|
26
|
+
def interrupt!(owner)
|
|
27
|
+
req = request_for(owner) or return
|
|
28
|
+
lock { (@interrupts ||= {})[owner] = true }
|
|
29
|
+
if persistent_http?(req.http)
|
|
30
|
+
close_socket(req.connection&.http)
|
|
31
|
+
req.http.finish(req.connection)
|
|
32
|
+
elsif transient_http?(req.http)
|
|
33
|
+
close_socket(req.http)
|
|
34
|
+
req.http.finish if req.http.active?
|
|
35
|
+
end
|
|
36
|
+
owner.stop if owner.respond_to?(:stop)
|
|
37
|
+
rescue *interrupt_errors
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
# Returns whether the active request is using a transient HTTP client.
|
|
54
|
-
# @param [Object, nil] http
|
|
55
|
-
# @return [Boolean]
|
|
56
|
-
def transient_http?(http)
|
|
57
|
-
Net::HTTP === http
|
|
58
|
-
end
|
|
41
|
+
private
|
|
59
42
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
##
|
|
44
|
+
# Closes the active socket for a request, if present.
|
|
45
|
+
# @param [Net::HTTP, nil] http
|
|
46
|
+
# @return [nil]
|
|
47
|
+
def close_socket(http)
|
|
48
|
+
socket = http&.instance_variable_get(:@socket) or return
|
|
49
|
+
socket = socket.io if socket.respond_to?(:io)
|
|
50
|
+
socket.close
|
|
51
|
+
rescue *interrupt_errors
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@requests[owner]
|
|
76
|
-
end
|
|
77
|
-
end
|
|
55
|
+
##
|
|
56
|
+
# Returns whether the active request is using a transient HTTP client.
|
|
57
|
+
# @param [Object, nil] http
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def transient_http?(http)
|
|
60
|
+
Net::HTTP === http
|
|
61
|
+
end
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@requests ||= {}
|
|
87
|
-
@requests[owner] = req
|
|
88
|
-
end
|
|
89
|
-
end
|
|
63
|
+
##
|
|
64
|
+
# Returns whether the active request is using a persistent HTTP client.
|
|
65
|
+
# @param [Object, nil] http
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def persistent_http?(http)
|
|
68
|
+
defined?(Net::HTTP::Persistent) && Net::HTTP::Persistent === http
|
|
69
|
+
end
|
|
90
70
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
##
|
|
72
|
+
# Returns the active request for an execution owner.
|
|
73
|
+
# @param [Fiber] owner
|
|
74
|
+
# @return [Request, nil]
|
|
75
|
+
def request_for(owner)
|
|
76
|
+
lock do
|
|
77
|
+
@requests ||= {}
|
|
78
|
+
@requests[owner]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
##
|
|
83
|
+
# Records an active request for an execution owner.
|
|
84
|
+
# @param [Request] req
|
|
85
|
+
# @param [Fiber] owner
|
|
86
|
+
# @return [Request]
|
|
87
|
+
def set_request(req, owner)
|
|
88
|
+
lock do
|
|
89
|
+
@requests ||= {}
|
|
90
|
+
@requests[owner] = req
|
|
106
91
|
end
|
|
107
92
|
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Clears the active request for an execution owner.
|
|
96
|
+
# @param [Fiber] owner
|
|
97
|
+
# @return [Request, nil]
|
|
98
|
+
def clear_request(owner)
|
|
99
|
+
lock { @requests&.delete(owner) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Returns whether an execution owner was interrupted.
|
|
104
|
+
# @param [Fiber] owner
|
|
105
|
+
# @return [Boolean, nil]
|
|
106
|
+
def interrupted?(owner)
|
|
107
|
+
lock { @interrupts&.delete(owner) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def optional_interrupt_errors
|
|
111
|
+
defined?(::Async::Stop) ? [Async::Stop] : []
|
|
112
|
+
end
|
|
108
113
|
end
|
|
109
114
|
end
|
|
@@ -50,9 +50,10 @@ class LLM::Provider
|
|
|
50
50
|
|
|
51
51
|
##
|
|
52
52
|
# Returns the current request owner.
|
|
53
|
-
# @return [
|
|
53
|
+
# @return [Object]
|
|
54
54
|
def request_owner
|
|
55
|
-
Fiber.current
|
|
55
|
+
return Fiber.current unless defined?(::Async)
|
|
56
|
+
Async::Task.current || Fiber.current
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
##
|
data/lib/llm/provider.rb
CHANGED
|
@@ -338,6 +338,14 @@ class LLM::Provider
|
|
|
338
338
|
end
|
|
339
339
|
alias_method :cancel!, :interrupt!
|
|
340
340
|
|
|
341
|
+
##
|
|
342
|
+
# Returns the current request owner used by the transport.
|
|
343
|
+
# @return [Object]
|
|
344
|
+
# @api private
|
|
345
|
+
def request_owner
|
|
346
|
+
transport.request_owner
|
|
347
|
+
end
|
|
348
|
+
|
|
341
349
|
##
|
|
342
350
|
# @param [Object] stream
|
|
343
351
|
# @return [Boolean]
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -12,7 +12,6 @@ module LLM::Sequel
|
|
|
12
12
|
module Agent
|
|
13
13
|
require_relative "plugin"
|
|
14
14
|
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
15
|
-
DEFAULT_USAGE_COLUMNS = LLM::Sequel::Plugin::DEFAULT_USAGE_COLUMNS
|
|
16
15
|
DEFAULTS = LLM::Sequel::Plugin::DEFAULTS
|
|
17
16
|
Utils = LLM::Sequel::Plugin::Utils
|
|
18
17
|
|
|
@@ -24,11 +23,8 @@ module LLM::Sequel
|
|
|
24
23
|
|
|
25
24
|
def self.configure(model, options = EMPTY_HASH, &block)
|
|
26
25
|
options = DEFAULTS.merge(options)
|
|
27
|
-
|
|
28
|
-
model.instance_variable_set(
|
|
29
|
-
:@llm_agent_options,
|
|
30
|
-
options.merge(usage_columns: usage_columns.freeze).freeze
|
|
31
|
-
)
|
|
26
|
+
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
27
|
+
model.instance_variable_set(:@llm_agent_options, options.freeze)
|
|
32
28
|
model.instance_exec(&block) if block
|
|
33
29
|
end
|
|
34
30
|
|
|
@@ -80,7 +76,6 @@ module LLM::Sequel
|
|
|
80
76
|
options = self.class.llm_plugin_options
|
|
81
77
|
columns = Agent::Utils.columns(options)
|
|
82
78
|
params = Agent::Utils.resolve_options(self, options[:context], Agent::EMPTY_HASH).dup
|
|
83
|
-
params[:model] ||= self[columns[:model_column]]
|
|
84
79
|
ctx = self.class.agent.new(llm, params.compact)
|
|
85
80
|
data = self[columns[:data_column]]
|
|
86
81
|
if data.nil? || data == ""
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -17,11 +17,6 @@ module LLM::Sequel
|
|
|
17
17
|
# can also be configured as symbols that are called on the model.
|
|
18
18
|
module Plugin
|
|
19
19
|
EMPTY_HASH = {}.freeze
|
|
20
|
-
DEFAULT_USAGE_COLUMNS = {
|
|
21
|
-
input_tokens: :input_tokens,
|
|
22
|
-
output_tokens: :output_tokens,
|
|
23
|
-
total_tokens: :total_tokens
|
|
24
|
-
}.freeze
|
|
25
20
|
|
|
26
21
|
##
|
|
27
22
|
# Shared helper methods for the ORM wrapper.
|
|
@@ -68,38 +63,46 @@ module LLM::Sequel
|
|
|
68
63
|
# Maps wrapper options onto the record's storage columns.
|
|
69
64
|
# @return [Hash]
|
|
70
65
|
def self.columns(options)
|
|
71
|
-
usage_columns = options[:usage_columns]
|
|
72
66
|
{
|
|
73
|
-
|
|
74
|
-
model_column: options[:model_column],
|
|
75
|
-
data_column: options[:data_column],
|
|
76
|
-
input_tokens: usage_columns[:input_tokens],
|
|
77
|
-
output_tokens: usage_columns[:output_tokens],
|
|
78
|
-
total_tokens: usage_columns[:total_tokens]
|
|
67
|
+
data_column: options[:data_column]
|
|
79
68
|
}.freeze
|
|
80
69
|
end
|
|
81
70
|
|
|
71
|
+
##
|
|
72
|
+
# Resolves the provider runtime for a record.
|
|
73
|
+
# @return [LLM::Provider]
|
|
74
|
+
def self.resolve_provider(obj, options, empty_hash)
|
|
75
|
+
provider = resolve_option(obj, options[:provider])
|
|
76
|
+
return provider if LLM::Provider === provider
|
|
77
|
+
raise ArgumentError, "provider: must resolve to an LLM::Provider instance"
|
|
78
|
+
end
|
|
79
|
+
|
|
82
80
|
##
|
|
83
81
|
# Persists the runtime state and usage columns back onto the record.
|
|
84
82
|
# @return [void]
|
|
85
83
|
def self.save(obj, ctx, options)
|
|
86
84
|
columns = self.columns(options)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
payload = serialize_context(ctx, options[:format])
|
|
86
|
+
payload = wrap_json_payload(payload, options[:format])
|
|
87
|
+
obj.update(columns[:data_column] => payload)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Wraps JSON payloads for Sequel PostgreSQL adapters when needed.
|
|
92
|
+
# @return [Object]
|
|
93
|
+
def self.wrap_json_payload(payload, format)
|
|
94
|
+
case format
|
|
95
|
+
when :json then Sequel.pg_json_wrap(payload)
|
|
96
|
+
when :jsonb then Sequel.pg_jsonb_wrap(payload)
|
|
97
|
+
else payload
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
end
|
|
95
101
|
DEFAULTS = {
|
|
96
|
-
provider_column: :provider,
|
|
97
|
-
model_column: :model,
|
|
98
102
|
data_column: :data,
|
|
99
103
|
format: :string,
|
|
100
|
-
usage_columns: DEFAULT_USAGE_COLUMNS,
|
|
101
104
|
tracer: nil,
|
|
102
|
-
provider:
|
|
105
|
+
provider: nil,
|
|
103
106
|
context: EMPTY_HASH
|
|
104
107
|
}.freeze
|
|
105
108
|
|
|
@@ -134,14 +137,13 @@ module LLM::Sequel
|
|
|
134
137
|
# @option options [Proc, Symbol, LLM::Tracer, nil] :tracer
|
|
135
138
|
# Optional tracer, method name, or proc that resolves to one and is
|
|
136
139
|
# assigned through `llm.tracer = ...` on the resolved provider.
|
|
140
|
+
# @option options [Proc, Symbol, LLM::Provider] :provider
|
|
141
|
+
# Must resolve to an `LLM::Provider` instance for the current record.
|
|
137
142
|
# @return [void]
|
|
138
143
|
def self.configure(model, options = EMPTY_HASH)
|
|
139
144
|
options = DEFAULTS.merge(options)
|
|
140
|
-
|
|
141
|
-
model.instance_variable_set(
|
|
142
|
-
:@llm_plugin_options,
|
|
143
|
-
options.merge(usage_columns: usage_columns.freeze).freeze
|
|
144
|
-
)
|
|
145
|
+
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
146
|
+
model.instance_variable_set(:@llm_plugin_options, options.freeze)
|
|
145
147
|
end
|
|
146
148
|
end
|
|
147
149
|
|
|
@@ -247,12 +249,7 @@ module LLM::Sequel
|
|
|
247
249
|
# Returns usage from the mapped usage columns.
|
|
248
250
|
# @return [LLM::Object]
|
|
249
251
|
def usage
|
|
250
|
-
|
|
251
|
-
LLM::Object.from(
|
|
252
|
-
input_tokens: self[columns[:input_tokens]] || 0,
|
|
253
|
-
output_tokens: self[columns[:output_tokens]] || 0,
|
|
254
|
-
total_tokens: self[columns[:total_tokens]] || 0
|
|
255
|
-
)
|
|
252
|
+
ctx.usage || LLM::Object.from(input_tokens: 0, output_tokens: 0, total_tokens: 0)
|
|
256
253
|
end
|
|
257
254
|
|
|
258
255
|
##
|
|
@@ -304,11 +301,8 @@ module LLM::Sequel
|
|
|
304
301
|
# @return [LLM::Provider]
|
|
305
302
|
def llm
|
|
306
303
|
options = self.class.llm_plugin_options
|
|
307
|
-
columns = Utils.columns(options)
|
|
308
|
-
provider = self[columns[:provider_column]]
|
|
309
|
-
kwargs = Utils.resolve_options(self, options[:provider], Plugin::EMPTY_HASH)
|
|
310
304
|
return @llm if @llm
|
|
311
|
-
@llm =
|
|
305
|
+
@llm = Utils.resolve_provider(self, options, Plugin::EMPTY_HASH)
|
|
312
306
|
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
313
307
|
@llm
|
|
314
308
|
end
|
|
@@ -322,7 +316,6 @@ module LLM::Sequel
|
|
|
322
316
|
options = self.class.llm_plugin_options
|
|
323
317
|
columns = Utils.columns(options)
|
|
324
318
|
params = Utils.resolve_options(self, options[:context], Plugin::EMPTY_HASH).dup
|
|
325
|
-
params[:model] ||= self[columns[:model_column]]
|
|
326
319
|
ctx = LLM::Context.new(llm, params.compact)
|
|
327
320
|
data = self[columns[:data_column]]
|
|
328
321
|
if data.nil? || data == ""
|
data/lib/llm/skill.rb
CHANGED
|
@@ -76,6 +76,8 @@ module LLM
|
|
|
76
76
|
def call(ctx)
|
|
77
77
|
instructions, tools, tracer = self.instructions, self.tools, ctx.llm.tracer
|
|
78
78
|
params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
|
|
79
|
+
concurrency = params[:stream].extra[:concurrency] if LLM::Stream === params[:stream]
|
|
80
|
+
params[:concurrency] = concurrency if concurrency
|
|
79
81
|
agent = Class.new(LLM::Agent) do
|
|
80
82
|
instructions(instructions)
|
|
81
83
|
tools(*tools)
|
|
@@ -98,6 +100,10 @@ module LLM
|
|
|
98
100
|
description skill.description
|
|
99
101
|
attr_accessor :tracer
|
|
100
102
|
|
|
103
|
+
define_singleton_method(:skill?) do
|
|
104
|
+
true
|
|
105
|
+
end
|
|
106
|
+
|
|
101
107
|
define_method(:call) do
|
|
102
108
|
skill.call(ctx)
|
|
103
109
|
end
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
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
|
+
version: 6.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antar Azri
|
|
@@ -236,6 +236,20 @@ dependencies:
|
|
|
236
236
|
- - "~>"
|
|
237
237
|
- !ruby/object:Gem::Version
|
|
238
238
|
version: '2.0'
|
|
239
|
+
- !ruby/object:Gem::Dependency
|
|
240
|
+
name: pg
|
|
241
|
+
requirement: !ruby/object:Gem::Requirement
|
|
242
|
+
requirements:
|
|
243
|
+
- - "~>"
|
|
244
|
+
- !ruby/object:Gem::Version
|
|
245
|
+
version: '1.5'
|
|
246
|
+
type: :development
|
|
247
|
+
prerelease: false
|
|
248
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
249
|
+
requirements:
|
|
250
|
+
- - "~>"
|
|
251
|
+
- !ruby/object:Gem::Version
|
|
252
|
+
version: '1.5'
|
|
239
253
|
description: |
|
|
240
254
|
llm.rb is a lightweight runtime for building capable AI systems in Ruby.
|
|
241
255
|
It is not just an API wrapper. llm.rb gives you one runtime for providers,
|