llm.rb 12.5.1 → 13.0.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +482 -0
  3. data/LICENSE +21 -93
  4. data/README.md +49 -159
  5. data/data/deepinfra.json +3 -0
  6. data/data/xai.json +1 -1
  7. data/lib/llm/a2a.rb +1 -1
  8. data/lib/llm/active_record/acts_as_agent.rb +32 -0
  9. data/lib/llm/active_record/acts_as_llm.rb +6 -6
  10. data/lib/llm/agent.rb +101 -26
  11. data/lib/llm/buffer.rb +85 -3
  12. data/lib/llm/compactor/null.rb +19 -0
  13. data/lib/llm/compactor/truncate.rb +80 -0
  14. data/lib/llm/compactor.rb +42 -124
  15. data/lib/llm/context.rb +33 -37
  16. data/lib/llm/contract.rb +4 -25
  17. data/lib/llm/function/array.rb +15 -14
  18. data/lib/llm/function/async/group.rb +54 -0
  19. data/lib/llm/function/async/reactor.rb +48 -0
  20. data/lib/llm/function/async/task.rb +83 -0
  21. data/lib/llm/function/fiber/group.rb +46 -0
  22. data/lib/llm/function/fiber/task.rb +62 -0
  23. data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
  24. data/lib/llm/function/fork/job.rb +4 -3
  25. data/lib/llm/function/fork/task.rb +20 -10
  26. data/lib/llm/function/group.rb +40 -0
  27. data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
  28. data/lib/llm/function/ractor/job.rb +19 -3
  29. data/lib/llm/function/ractor/mailbox.rb +9 -0
  30. data/lib/llm/function/ractor/task.rb +24 -15
  31. data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
  32. data/lib/llm/function/sequential/task.rb +49 -0
  33. data/lib/llm/function/task.rb +25 -37
  34. data/lib/llm/function/thread/group.rb +46 -0
  35. data/lib/llm/function/thread/task.rb +60 -0
  36. data/lib/llm/function/tracing.rb +2 -0
  37. data/lib/llm/function.rb +56 -64
  38. data/lib/llm/loop_guard.rb +1 -2
  39. data/lib/llm/mcp.rb +22 -0
  40. data/lib/llm/object.rb +2 -1
  41. data/lib/llm/provider.rb +6 -3
  42. data/lib/llm/providers/google.rb +2 -2
  43. data/lib/llm/repl/command.rb +35 -8
  44. data/lib/llm/repl/commands/compact.rb +33 -0
  45. data/lib/llm/repl/input.rb +80 -15
  46. data/lib/llm/repl/markdown/table.rb +76 -0
  47. data/lib/llm/repl/markdown.rb +31 -1
  48. data/lib/llm/repl/status.rb +1 -1
  49. data/lib/llm/repl/stream.rb +10 -3
  50. data/lib/llm/repl/transcript.rb +1 -1
  51. data/lib/llm/repl/walker.rb +46 -0
  52. data/lib/llm/repl.rb +18 -12
  53. data/lib/llm/response.rb +10 -0
  54. data/lib/llm/schema/leaf.rb +5 -0
  55. data/lib/llm/schema/object.rb +11 -5
  56. data/lib/llm/sequel/agent.rb +32 -0
  57. data/lib/llm/sequel/plugin.rb +6 -6
  58. data/lib/llm/stream.rb +24 -17
  59. data/lib/llm/tool/param.rb +12 -0
  60. data/lib/llm/tool.rb +20 -4
  61. data/lib/llm/tools/chdir.rb +0 -2
  62. data/lib/llm/tools/git.rb +8 -4
  63. data/lib/llm/tools/mkdir.rb +1 -1
  64. data/lib/llm/tools/pwd.rb +0 -2
  65. data/lib/llm/tools/read_file.rb +0 -2
  66. data/lib/llm/tools/rg.rb +8 -4
  67. data/lib/llm/tools/shell.rb +8 -4
  68. data/lib/llm/tools/utils.rb +31 -0
  69. data/lib/llm/version.rb +1 -1
  70. data/lib/llm.rb +25 -5
  71. data/llm.gemspec +3 -3
  72. data/resources/deepdive.md +693 -57
  73. metadata +24 -13
  74. data/lib/llm/function/call_task.rb +0 -46
  75. data/lib/llm/function/fiber_group.rb +0 -105
  76. data/lib/llm/function/task_group.rb +0 -97
  77. data/lib/llm/function/thread_group.rb +0 -102
data/lib/llm/stream.rb CHANGED
@@ -11,6 +11,21 @@ module LLM
11
11
  # a small helper for collecting asynchronous tool work started from a
12
12
  # callback.
13
13
  #
14
+ # @example Subclass with callbacks
15
+ # class MyStream < LLM::Stream
16
+ # def on_content(content)
17
+ # print content
18
+ # end
19
+ #
20
+ # def on_reasoning_content(content)
21
+ # warn content
22
+ # end
23
+ # end
24
+ #
25
+ # llm = LLM.deepseek(key: ENV["KEY"])
26
+ # agent = LLM::Agent.new(llm, stream: MyStream.new)
27
+ # agent.talk "Explain Ruby fibers."
28
+ #
14
29
  # @note The `on_*` callbacks run inline with the streaming parser. They
15
30
  # therefore block streaming progress and should generally return as
16
31
  # quickly as possible.
@@ -19,6 +34,9 @@ module LLM
19
34
  # Providers may also call {#on_reasoning_content} and {#on_tool_call} when
20
35
  # that data is available. Runtime features such as context compaction may
21
36
  # also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
37
+ #
38
+ # @see LLM::Agent Where streams are typically attached
39
+ # @see LLM::Context Where streams are bound per-turn
22
40
  class Stream
23
41
  require_relative "stream/queue"
24
42
  require_relative "stream/io"
@@ -109,16 +127,8 @@ module LLM
109
127
 
110
128
  ##
111
129
  # Called when a streamed tool call has been fully constructed.
112
- # @note A stream implementation may start tool execution here, for
113
- # example by pushing `ctx.spawn(tool, :thread)`,
114
- # `ctx.spawn(tool, :fiber)`, or `ctx.spawn(tool, :task)` onto {#queue}.
115
- # Mixed strategies can also be selected per tool, such as
116
- # `tool.mcp? ? ctx.spawn(tool, :task) : ctx.spawn(tool, :ractor)`.
117
- # Streamed tool resolution now prefers the current request tools, so
118
- # {LLM.function}, MCP tools, bound tool instances, and normal
119
- # {LLM::Tool LLM::Tool} classes can all resolve through the same
120
- # request-local path. The current `:ractor` mode is for class-based
121
- # tools and does not support MCP tools.
130
+ # A stream implementation may start tool execution here by pushing
131
+ # `@queue << tool.task(:thread)` onto {#queue}.
122
132
  # @param [LLM::Function] tool
123
133
  # The parsed tool call.
124
134
  # @return [nil]
@@ -128,9 +138,8 @@ module LLM
128
138
 
129
139
  ##
130
140
  # Called when queued streamed tool work returns.
131
- # @note This callback runs when {#wait} resolves work that was queued from
132
- # {#on_tool_call}, such as values returned by `ctx.spawn(tool, :thread)`,
133
- # `ctx.spawn(tool, :fiber)`, or `ctx.spawn(tool, :task)`.
141
+ # This callback runs when {#wait} resolves work queued from
142
+ # {#on_tool_call}.
134
143
  # @param [LLM::Function] tool
135
144
  # The tool that returned.
136
145
  # @param [LLM::Function::Return] result
@@ -160,19 +169,17 @@ module LLM
160
169
 
161
170
  ##
162
171
  # Called before a context compaction starts.
163
- # @param [LLM::Context] ctx
164
172
  # @param [LLM::Compactor] compactor
165
173
  # @return [nil]
166
- def on_compaction(ctx, compactor)
174
+ def on_compaction(compactor)
167
175
  nil
168
176
  end
169
177
 
170
178
  ##
171
179
  # Called after a context compaction finishes.
172
- # @param [LLM::Context] ctx
173
180
  # @param [LLM::Compactor] compactor
174
181
  # @return [nil]
175
- def on_compaction_finish(ctx, compactor)
182
+ def on_compaction_finish(compactor)
176
183
  nil
177
184
  end
178
185
 
@@ -56,6 +56,18 @@ class LLM::Tool
56
56
  end
57
57
  end
58
58
 
59
+ ##
60
+ # Set default values for parameters.
61
+ # @param [Hash] defaults
62
+ # @return [LLM::Schema::Object]
63
+ def defaults(defaults)
64
+ lock do
65
+ function.params.tap do |schema|
66
+ defaults.each { Utils.fetch(schema.properties, _1).default(_2) }
67
+ end
68
+ end
69
+ end
70
+
59
71
  ##
60
72
  # @api private
61
73
  module Utils
data/lib/llm/tool.rb CHANGED
@@ -5,10 +5,23 @@
5
5
  # that can be called by an LLM. Under the hood, it is a wrapper
6
6
  # around {LLM::Function LLM::Function} but allows the definition
7
7
  # of a function (also known as a tool) as a class.
8
- # @example
9
- # class System < LLM::Tool
10
- # name "system"
11
- # description "Runs system commands"
8
+ #
9
+ # @example Declarative form (preferred)
10
+ # class ReadFile < LLM::Tool
11
+ # name "read-file"
12
+ # description "Read a file from disk"
13
+ # parameter :path, String, "The filename or path"
14
+ # required %i[path]
15
+ #
16
+ # def call(path:)
17
+ # {contents: File.read(path)}
18
+ # end
19
+ # end
20
+ #
21
+ # @example Block-form DSL (also supported)
22
+ # class RunCommand < LLM::Tool
23
+ # name "run-command"
24
+ # description "Runs a shell command"
12
25
  # params do |schema|
13
26
  # schema.object(command: schema.string.required)
14
27
  # end
@@ -17,6 +30,9 @@
17
30
  # {success: Kernel.system(command)}
18
31
  # end
19
32
  # end
33
+ #
34
+ # @see LLM::Agent Tools are attached to agents
35
+ # @see LLM::Function The function object that Tool wraps
20
36
  class LLM::Tool
21
37
  require_relative "tool/param"
22
38
  extend LLM::Tool::Param
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::Chdir LLM::Tool::Chdir} class implements
data/lib/llm/tools/git.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::Git LLM::Tool::Git} class implements
@@ -9,18 +7,24 @@ class LLM::Tool
9
7
  # The actions it can perform are read-only - at least for
10
8
  # the time being.
11
9
  class Git < self
10
+ require_relative "utils"
11
+ include Utils
12
+
12
13
  name "git"
13
14
  description "perform an action with git"
14
15
  parameter :action, Enum["log", "diff", "commit", "checkout", "branch", "show"], "the git operation to perform"
15
16
  parameter :arguments, Array[String], "one or more arguments for the git action"
17
+ parameter :timeout, Integer, "the maximum time to allow the command to run"
16
18
  required %i[action]
19
+ defaults arguments: [], timeout: 5
17
20
 
18
21
  ##
19
22
  # @param [String] action
20
23
  # @param [Array<String>, nil] arguments
21
24
  # @return [Hash]
22
- def call(action:, arguments: nil)
25
+ def call(action:, arguments: [], timeout: 5)
23
26
  command = spawn(action:, arguments:)
27
+ wait(command:, timeout:)
24
28
  {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
25
29
  end
26
30
 
@@ -34,7 +38,7 @@ class LLM::Tool
34
38
  .spawn
35
39
  end
36
40
 
37
- LLM.require "test-cmd.rb"
41
+ LLM.require "test-cmd.rb", "~> 1.1"
38
42
  Command = Test::Cmd
39
43
  end
40
44
  end
@@ -26,7 +26,7 @@ class LLM::Tool
26
26
  .spawn
27
27
  end
28
28
 
29
- LLM.require "test-cmd.rb"
29
+ LLM.require "test-cmd.rb", "~> 1.1"
30
30
  Command = Test::Cmd
31
31
  end
32
32
  end
data/lib/llm/tools/pwd.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::Pwd LLM::Tool::Pwd} class implements
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::ReadFile LLM::Tool::ReadFile} class implements
data/lib/llm/tools/rg.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::Rg LLM::Tool::Rg} class implements
@@ -9,19 +7,25 @@ class LLM::Tool
9
7
  # recursively search the current working directory
10
8
  # for one or more patterns.
11
9
  class Rg < self
10
+ require_relative "utils"
11
+ include Utils
12
+
12
13
  name "rg"
13
14
  description "recursively search the current directory for lines matching a pattern"
14
15
  parameter :patterns, Array[String], "one or more search patterns"
15
16
  parameter :path, String, "the path where the search is performed (default is cwd)"
17
+ parameter :timeout, Integer, "the number of seconds to wait before cancelling the action"
16
18
  required %i[patterns]
19
+ defaults path: Dir.getwd, timeout: 5
17
20
 
18
21
  ##
19
22
  # @param [Array<String>] patterns
20
23
  # @param [String] path
21
24
  # @return [Hash]
22
- def call(patterns:, path: Dir.getwd)
25
+ def call(patterns:, path: Dir.getwd, timeout: 5)
23
26
  validate!(patterns:, path:)
24
27
  command = spawn(patterns:, path:)
28
+ wait(command:, timeout:)
25
29
  {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
26
30
  end
27
31
 
@@ -41,7 +45,7 @@ class LLM::Tool
41
45
  .spawn
42
46
  end
43
47
 
44
- LLM.require "test-cmd.rb"
48
+ LLM.require "test-cmd.rb", "~> 1.1"
45
49
  Command = Test::Cmd
46
50
  end
47
51
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_literal
4
-
5
3
  class LLM::Tool
6
4
  ##
7
5
  # The {LLM::Tool::Shell} class implements a tool that can
@@ -12,11 +10,16 @@ class LLM::Tool
12
10
  # by managing the tool loop manually through
13
11
  # {LLM::Context LLM::Context}.
14
12
  class Shell < self
13
+ require_relative "utils"
14
+ include Utils
15
+
15
16
  name "shell"
16
17
  description "run a shell command"
17
18
  parameter :name, String, "the command name"
18
19
  parameter :arguments, Array[String], "one or more command arguments"
20
+ parameter :timeout, Integer, "the maximum allowed time for the command to run (in seconds)"
19
21
  required %i[name]
22
+ defaults arguments: [], timeout: 60
20
23
 
21
24
  ##
22
25
  # @param [String] name
@@ -24,8 +27,9 @@ class LLM::Tool
24
27
  # @param [Array<String>] arguments
25
28
  # One or more command-line arguments
26
29
  # @return [Hash]
27
- def call(name:, arguments: nil)
30
+ def call(name:, arguments: [], timeout: 60)
28
31
  command = spawn(name:, arguments:)
32
+ wait(command:, timeout:)
29
33
  {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
30
34
  end
31
35
 
@@ -42,7 +46,7 @@ class LLM::Tool
42
46
  .spawn
43
47
  end
44
48
 
45
- LLM.require "test-cmd.rb"
49
+ LLM.require "test-cmd.rb", "~> 1.1"
46
50
  Command = Test::Cmd
47
51
  end
48
52
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # Shared utilities for tool implementations.
6
+ module Utils
7
+ ##
8
+ # Wait for a command to finish, or abort
9
+ # with an error when it exceeds the
10
+ # specified timeout.
11
+ # @param [Test::Cmd] command
12
+ # @param [Integer] timeout
13
+ # @return [void]
14
+ def wait(command:, timeout:)
15
+ start = now
16
+ while command.running?
17
+ if now - start > timeout
18
+ command.kill!
19
+ raise "command timed out after #{timeout}s"
20
+ end
21
+ sleep 0.01
22
+ end
23
+ end
24
+
25
+ ##
26
+ # @return [Numeric]
27
+ def now
28
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ end
30
+ end
31
+ 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 = "12.5.1"
4
+ VERSION = "13.0.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -1,8 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # llm.rb is a zero-dependency AI runtime for Ruby. Twelve providers, six
5
+ # concurrency strategies, MCP and A2A, streaming tool calls with
6
+ # cancellation, context compaction, and ORM persistence.
7
+ #
8
+ # @example The three-step workflow
9
+ # require "llm"
10
+ # llm = LLM.deepseek(key: ENV["KEY"]) # 1. pick a provider
11
+ # agent = LLM::Agent.new(llm, stream: $stdout) # 2. create an agent
12
+ # agent.talk "Hello world" # 3. talk to it
13
+ #
14
+ # @see LLM::Agent The recommended high-level interface
15
+ # @see LLM::Context The low-level stateful runtime (advanced)
3
16
  module LLM
4
17
  require "stringio"
5
18
  require "securerandom"
19
+ require_relative "llm/compactor"
6
20
  require_relative "llm/json_adapter"
7
21
  require_relative "llm/tracer"
8
22
  require_relative "llm/error"
@@ -49,15 +63,21 @@ module LLM
49
63
 
50
64
  ##
51
65
  # Requires an optional runtime dependency
52
- # @raise [LLM::DependencyError]
66
+ # @param [String] name
67
+ # The name of a gem
68
+ # @param [String, nil] version
69
+ # Optional gem version
70
+ # @raise [LLM::LoadError]
53
71
  # When the dependency cannot be loaded
54
- def self.require(name)
55
- super
56
- rescue ::LoadError
72
+ def self.require(name, version = nil)
57
73
  names = {"xchan" => "xchan.rb", "net/http/persistent" => "net-http-persistent"}
74
+ gem(names[name] || name, version) if version
75
+ super(name)
76
+ rescue ::LoadError
58
77
  name = names[name] || name
59
78
  raise LLM::LoadError,
60
- "#{name} is an optional runtime dependency but it does not appear to be installed. " \
79
+ "#{name}#{version ? " #{version}" : ""} is an optional " \
80
+ "runtime dependency but it does not appear to be installed. " \
61
81
  "Consider 'gem install #{name}', adding '#{name}' to your Gemfile or " \
62
82
  "opting out of the functionality provided by '#{name}'"
63
83
  end
data/llm.gemspec CHANGED
@@ -16,7 +16,7 @@ functionality &ndash; such as ActiveRecord support &ndash; require
16
16
  optional dependencies that are opt-in.
17
17
  DESCRIPTION
18
18
 
19
- spec.license = "BUSL-1.1"
19
+ spec.license = "MIT"
20
20
  spec.required_ruby_version = ">= 3.3.0"
21
21
 
22
22
  spec.homepage = "https://r.uby.dev/llm/"
@@ -38,7 +38,7 @@ DESCRIPTION
38
38
  spec.add_development_dependency "yard", "~> 0.9.37"
39
39
  spec.add_development_dependency "redcarpet", "~> 3.6"
40
40
  spec.add_development_dependency "webrick", "~> 1.8"
41
- spec.add_development_dependency "test-cmd.rb", "~> 0.12.0"
41
+ spec.add_development_dependency "test-cmd.rb", "~> 1.1.0"
42
42
  spec.add_development_dependency "rake", "~> 13.0"
43
43
  spec.add_development_dependency "rspec", "~> 3.0"
44
44
  spec.add_development_dependency "standard", "~> 1.50"
@@ -50,7 +50,7 @@ DESCRIPTION
50
50
  spec.add_development_dependency "activerecord", "~> 8.0"
51
51
  spec.add_development_dependency "sequel", "~> 5.0"
52
52
  spec.add_development_dependency "sqlite3", "~> 2.0"
53
- spec.add_development_dependency "xchan.rb", "~> 0.20"
53
+ spec.add_development_dependency "xchan.rb", "~> 0.22"
54
54
  spec.add_development_dependency "pg", "~> 1.5"
55
55
  spec.add_development_dependency "irb", "~> 1.18"
56
56
  end