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.
@@ -26,6 +26,7 @@ class LLM::Context
26
26
  LLM.json.load(string)
27
27
  end
28
28
  @messages.concat [*ctx["messages"]].map { deserialize_message(_1) }
29
+ @compacted = !!ctx["compacted"]
29
30
  self
30
31
  end
31
32
  alias_method :restore, :deserialize
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 = Fiber.current
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 = Fiber.current
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
- # @note
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
- return unless usage
331
- LLM::Object.from(
332
- input_tokens: usage.input_tokens || 0,
333
- output_tokens: usage.output_tokens || 0,
334
- reasoning_tokens: usage.reasoning_tokens || 0,
335
- total_tokens: usage.total_tokens || 0
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
- {schema_version: 1, model:, messages: @messages.map { serialize_message(_1) }}
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
- LLM::Cost.new(
438
- (cost.input.to_f / 1_000_000.0) * usage.input_tokens,
439
- (cost.output.to_f / 1_000_000.0) * usage.output_tokens
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 ArgumentError, "Ractor concurrency only supports class-based tools" unless Class === @runner
232
- Ractor::Task.new(@runner, id, name, arguments)
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 *LLM::Provider::Transport::HTTP::Interruptible::INTERRUPT_ERRORS
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
- module Transport
5
- class HTTP
6
- ##
7
- # Internal request interruption methods for
8
- # {LLM::Provider::Transport::HTTP}.
9
- #
10
- # This module tracks active requests by execution owner and provides
11
- # the logic used to interrupt an in-flight request by closing the
12
- # active HTTP connection.
13
- #
14
- # @api private
15
- module Interruptible
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
- # Interrupt an active request, if any.
21
- # @param [Fiber] owner
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
- # Closes the active socket for a request, if present.
42
- # @param [Net::HTTP, nil] http
43
- # @return [nil]
44
- def close_socket(http)
45
- socket = http&.instance_variable_get(:@socket) or return
46
- socket = socket.io if socket.respond_to?(:io)
47
- socket.close
48
- rescue *INTERRUPT_ERRORS
49
- nil
50
- end
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
- # Returns whether the active request is using a persistent HTTP client.
62
- # @param [Object, nil] http
63
- # @return [Boolean]
64
- def persistent_http?(http)
65
- defined?(Net::HTTP::Persistent) && Net::HTTP::Persistent === http
66
- end
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
- # Returns the active request for an execution owner.
70
- # @param [Fiber] owner
71
- # @return [Request, nil]
72
- def request_for(owner)
73
- lock do
74
- @requests ||= {}
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
- # Records an active request for an execution owner.
81
- # @param [Request] req
82
- # @param [Fiber] owner
83
- # @return [Request]
84
- def set_request(req, owner)
85
- lock do
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
- # Clears the active request for an execution owner.
93
- # @param [Fiber] owner
94
- # @return [Request, nil]
95
- def clear_request(owner)
96
- lock { @requests&.delete(owner) }
97
- end
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
- # Returns whether an execution owner was interrupted.
101
- # @param [Fiber] owner
102
- # @return [Boolean, nil]
103
- def interrupted?(owner)
104
- lock { @interrupts&.delete(owner) }
105
- end
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 [Fiber]
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]
@@ -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
- usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
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 == ""
@@ -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
- provider_column: options[:provider_column],
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
- obj.update(
88
- columns[:data_column] => serialize_context(ctx, options[:format]),
89
- columns[:input_tokens] => ctx.usage.input_tokens,
90
- columns[:output_tokens] => ctx.usage.output_tokens,
91
- columns[:total_tokens] => ctx.usage.total_tokens
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: EMPTY_HASH,
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
- usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
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
- columns = Utils.columns(self.class.llm_plugin_options)
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 = LLM.method(provider).call(**kwargs)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "5.4.0"
4
+ VERSION = "6.1.0"
5
5
  end
data/llm.gemspec CHANGED
@@ -57,4 +57,5 @@ Gem::Specification.new do |spec|
57
57
  spec.add_development_dependency "activerecord", "~> 8.0"
58
58
  spec.add_development_dependency "sequel", "~> 5.0"
59
59
  spec.add_development_dependency "sqlite3", "~> 2.0"
60
+ spec.add_development_dependency "pg", "~> 1.5"
60
61
  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: 5.4.0
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,