llm.rb 15.4.0 → 15.5.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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Schema
4
+ ##
5
+ # {LLM::Schema::Deferred LLM::Schema::Deferred} is a placeholder for a
6
+ # leaf that is resolved when the schema is rendered.
7
+ #
8
+ # A parameter type that can only be known at runtime - an enum of the
9
+ # keys a store holds right now, for example - is given as a proc. The
10
+ # proc is called when the schema is rendered rather than when the class
11
+ # that declares the parameter is defined, so declaring a tool does not
12
+ # touch whatever the type depends on:
13
+ #
14
+ # parameter :fruit, proc { Enum[*Fruit.kinds]}, "A fruit", required: true
15
+ #
16
+ # Whatever the proc returns is resolved in turn, so it can return a
17
+ # leaf, a class, or an array of either.
18
+ #
19
+ # @api private
20
+ class Deferred < Leaf
21
+ ##
22
+ # @param [Proc] block
23
+ # A block that returns the leaf to resolve to
24
+ # @return [LLM::Schema::Deferred]
25
+ def initialize(&block)
26
+ super()
27
+ @block = block
28
+ end
29
+
30
+ ##
31
+ # @return [Hash]
32
+ def to_h
33
+ resolved_leaf.to_h
34
+ end
35
+
36
+ ##
37
+ # @param [Hash] options
38
+ # @return [String]
39
+ def to_json(options = {})
40
+ resolved_leaf.to_h.to_json(options)
41
+ end
42
+
43
+ ##
44
+ # @return [String]
45
+ def to_s
46
+ resolved_leaf.to_s
47
+ end
48
+
49
+ private
50
+
51
+ ##
52
+ # Returns the leaf the block resolves to, with the settings that
53
+ # were given to the placeholder applied to it.
54
+ #
55
+ # The settings live on the placeholder, because they are given
56
+ # before the leaf it stands for exists.
57
+ # @return [LLM::Schema::Leaf]
58
+ def resolved_leaf
59
+ leaf = @block.call
60
+ leaf.description(@description) if @description
61
+ leaf.default(@default) unless @default.nil?
62
+ leaf.enum(*@enum) if @enum
63
+ leaf.const(@const) unless @const.nil?
64
+ leaf.required if required?
65
+ leaf
66
+ end
67
+ end
68
+ end
@@ -12,6 +12,9 @@ class LLM::Schema
12
12
  # @return [Integer, nil]
13
13
  attr_accessor :index
14
14
 
15
+ ##
16
+ # Returns a leaf.
17
+ # @return [LLM::Schema::Leaf]
15
18
  def initialize
16
19
  @description = nil
17
20
  @default = nil
data/lib/llm/schema.rb CHANGED
@@ -36,6 +36,7 @@ class LLM::Schema
36
36
  require_relative "schema/parser"
37
37
  require_relative "schema/renderer"
38
38
  require_relative "schema/leaf"
39
+ require_relative "schema/deferred"
39
40
  require_relative "schema/object"
40
41
  require_relative "schema/array"
41
42
  require_relative "schema/all_of"
@@ -56,9 +57,21 @@ class LLM::Schema
56
57
  module Utils
57
58
  extend self
58
59
 
60
+ ##
61
+ # Resolves a parameter type into a leaf of the schema.
62
+ #
63
+ # A proc is called for its type, and whatever it returns is resolved
64
+ # in turn, so a type that can only be known at runtime - an enum of
65
+ # the keys a store holds right now - can be declared without being
66
+ # evaluated when the class that declares it is defined.
67
+ # @param [LLM::Schema] schema
68
+ # @param [Object] type
69
+ # @return [LLM::Schema::Leaf]
59
70
  def resolve(schema, type)
60
71
  if LLM::Schema::Leaf === type
61
72
  type
73
+ elsif ::Proc === type
74
+ Deferred.new { resolve(schema, type.call) }
62
75
  elsif ::Array === type
63
76
  resolve_array(schema, type)
64
77
  elsif Class === type && type.respond_to?(:object)
@@ -22,8 +22,10 @@ class LLM::Tool
22
22
  ##
23
23
  # @param name [Symbol]
24
24
  # The name of a parameter
25
- # @param type [LLM::Schema::Leaf, Class]
26
- # The parameter type (eg String)
25
+ # @param type [LLM::Schema::Leaf, Class, Proc]
26
+ # The parameter type (eg String). A proc is called for the type
27
+ # when the parameters are read, so a type that can only be known
28
+ # at runtime does not have to be known when the class is defined.
27
29
  # @param description [String]
28
30
  # The description of a property
29
31
  # @param options [Hash]
@@ -54,7 +54,8 @@ class LLM::Tool
54
54
  # the max number of bytes to emit
55
55
  # @return [Hash]
56
56
  def call(arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
57
- name = arguments[0]
57
+ startat = now
58
+ name = arguments[0]
58
59
  command = spawn(name:, arguments: arguments[1..], env:, max_bytes:)
59
60
  wait(command:, timeout:)
60
61
  if command.not_found?
@@ -62,7 +63,8 @@ class LLM::Tool
62
63
  else
63
64
  {ok: command.success?,
64
65
  stdout: truncate(command.stdout, max_bytes:),
65
- stderr: truncate(command.stderr, max_bytes:)}
66
+ stderr: truncate(command.stderr, max_bytes:),
67
+ duration: "#{(now - startat).round(1)} seconds"}
66
68
  end
67
69
  rescue LLM::Interrupt
68
70
  command.kill! if command&.running?
@@ -72,5 +74,11 @@ class LLM::Tool
72
74
  private
73
75
 
74
76
  attr_reader :env
77
+
78
+ ##
79
+ # @return [Float]
80
+ def now
81
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ end
75
83
  end
76
84
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tracer
4
+ ##
5
+ # {LLM::Tracer::Registry LLM::Tracer::Registry} counts the scopes that
6
+ # are open for each tracer.
7
+ #
8
+ # The count belongs to the tracer, not to the thread that opened a
9
+ # scope. A tool runs on a thread of its own and opens a scope there for
10
+ # the turn's tracer, and a count kept per thread would let that scope
11
+ # look like the outermost one: it would end the tracer while the turn
12
+ # is still running, and every write after it would fail.
13
+ #
14
+ # @api private
15
+ class Registry
16
+ ##
17
+ # @return [LLM::Tracer::Registry]
18
+ def initialize
19
+ @counts = ObjectSpace::WeakMap.new
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ ##
24
+ # Records that a scope was opened for a tracer.
25
+ # @param [LLM::Tracer] tracer
26
+ # @return [LLM::Tracer::Registry]
27
+ # Returns self, so a caller can tell the scope was recorded
28
+ def enter(tracer)
29
+ @mutex.synchronize do
30
+ @counts[tracer] = count(tracer) + 1
31
+ end
32
+ self
33
+ end
34
+
35
+ ##
36
+ # Records that a scope was closed, and tells the tracer it is
37
+ # finished when it was the last one that was open.
38
+ #
39
+ # The count is brought up to date under the lock, and the tracer is
40
+ # told after the lock is released: a tracer that calls back into the
41
+ # provider from `on_exit` would otherwise deadlock against itself.
42
+ #
43
+ # An `exit` without a matching `enter` is ignored: no scope is open,
44
+ # so there is nothing to end, and the tracer is not told it is
45
+ # finished when no scope was holding it.
46
+ # @param [LLM::Tracer] tracer
47
+ # @return [void]
48
+ def exit(tracer)
49
+ last = @mutex.synchronize do
50
+ open = count(tracer)
51
+ if open.zero?
52
+ false
53
+ elsif open == 1
54
+ forget(tracer)
55
+ true
56
+ else
57
+ @counts[tracer] = open - 1
58
+ false
59
+ end
60
+ end
61
+ tracer.on_exit if last
62
+ end
63
+
64
+ private
65
+
66
+ ##
67
+ # Drops a tracer that has no open scopes left.
68
+ # @param [LLM::Tracer] tracer
69
+ # @return [Boolean]
70
+ def forget(tracer)
71
+ if @counts.respond_to?(:delete)
72
+ @counts.delete(tracer)
73
+ else
74
+ @counts[tracer] = 0
75
+ end
76
+ true
77
+ end
78
+
79
+ ##
80
+ # @param [LLM::Tracer] tracer
81
+ # @return [Integer]
82
+ def count(tracer)
83
+ @counts[tracer] || 0
84
+ end
85
+ end
86
+ end
data/lib/llm/tracer.rb CHANGED
@@ -13,6 +13,39 @@ module LLM
13
13
  require_relative "tracer/telemetry"
14
14
  require_relative "tracer/null"
15
15
  require_relative "tracer/pretty_logger"
16
+ require_relative "tracer/registry"
17
+
18
+ ##
19
+ # Returns the registry that counts the open scopes for each tracer.
20
+ # @api private
21
+ # @return [LLM::Tracer::Registry]
22
+ def self.registry
23
+ LLM.lock(:registry) do
24
+ @registry ||= Registry.new
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Records that a scope was opened for a tracer.
30
+ # @api private
31
+ # @see LLM::Tracer::Registry#enter
32
+ # @param [LLM::Tracer] tracer
33
+ # @return [LLM::Tracer::Registry]
34
+ # Returns the registry, so a caller can tell the scope was recorded
35
+ def self.enter(tracer)
36
+ registry.enter(tracer)
37
+ end
38
+
39
+ ##
40
+ # Records that a scope was closed for a tracer, and tells the tracer
41
+ # it is finished when it was the last one that was open.
42
+ # @api private
43
+ # @see LLM::Tracer::Registry#exit
44
+ # @param [LLM::Tracer] tracer
45
+ # @return [void]
46
+ def self.exit(tracer)
47
+ registry.exit(tracer)
48
+ end
16
49
 
17
50
  ##
18
51
  # Builds a {LLM::Tracer::PrettyLogger} for a provider.
@@ -61,6 +94,29 @@ module LLM
61
94
  @options = {}
62
95
  end
63
96
 
97
+ ##
98
+ # Called when the tracer is no longer in use.
99
+ #
100
+ # A tracer that holds something - a file, a span, a connection pool -
101
+ # releases it here. The default does nothing, because most tracers hold
102
+ # nothing that needs releasing.
103
+ #
104
+ # It is called once, when the last scope that is open for this tracer
105
+ # ends, and that scope can have been opened by another thread: a tool
106
+ # runs on a thread of its own and scopes the turn's tracer while it
107
+ # does. It is called after the scoped lookup is restored, so a tracer
108
+ # that reads {LLM::Provider#tracer} inside it sees the tracer that the
109
+ # next request will see.
110
+ #
111
+ # A tracer can be scoped again afterwards, on a later turn, so it has
112
+ # to stay usable after this call, and it has to tolerate the call
113
+ # happening more than once over its life.
114
+ # @see LLM::Tracer::Registry
115
+ # @see LLM::Provider#with_tracer
116
+ # @return [void]
117
+ def on_exit
118
+ end
119
+
64
120
  ##
65
121
  # Called before an LLM provider request is executed.
66
122
  # @param [String] operation
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "15.4.0"
4
+ VERSION = "15.5.0"
5
5
  end
data/llm.gemspec CHANGED
@@ -34,12 +34,6 @@ DESCRIPTION
34
34
  ]
35
35
  spec.executables = ["llm.rb"]
36
36
  spec.require_paths = ["lib"]
37
- spec.post_install_message = "\n" \
38
- "Want to see what I'm building with llm.rb? " \
39
- "\n" \
40
- "Checkout the https://r.uby.dev website." \
41
- "\n\n"
42
-
43
37
  spec.add_development_dependency "webmock", "~> 3.24.0"
44
38
  spec.add_development_dependency "yard", "~> 0.9.37"
45
39
  spec.add_development_dependency "redcarpet", "~> 3.6"
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: 15.4.0
4
+ version: 15.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gleeson
@@ -624,6 +624,7 @@ files:
624
624
  - lib/llm/schema/any_of.rb
625
625
  - lib/llm/schema/array.rb
626
626
  - lib/llm/schema/boolean.rb
627
+ - lib/llm/schema/deferred.rb
627
628
  - lib/llm/schema/enum.rb
628
629
  - lib/llm/schema/integer.rb
629
630
  - lib/llm/schema/leaf.rb
@@ -665,6 +666,7 @@ files:
665
666
  - lib/llm/tracer/logger.rb
666
667
  - lib/llm/tracer/null.rb
667
668
  - lib/llm/tracer/pretty_logger.rb
669
+ - lib/llm/tracer/registry.rb
668
670
  - lib/llm/tracer/telemetry.rb
669
671
  - lib/llm/transformer.rb
670
672
  - lib/llm/transformer/null.rb
@@ -695,8 +697,6 @@ metadata:
695
697
  source_code_uri: https://github.com/r-uby-dev/llm
696
698
  documentation_uri: https://r.uby.dev
697
699
  changelog_uri: https://github.com/r-uby-dev/llm/blob/main/CHANGELOG.md
698
- post_install_message: "\nWant to see what I'm building with llm.rb? \nCheckout the
699
- https://r.uby.dev website.\n\n"
700
700
  rdoc_options: []
701
701
  require_paths:
702
702
  - lib