llm.rb 12.6.0 → 13.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +571 -13
  3. data/LICENSE +21 -93
  4. data/README.md +183 -167
  5. data/bin/llm.rb +124 -0
  6. data/data/deepinfra.json +3 -0
  7. data/data/xai.json +1 -1
  8. data/lib/llm/a2a.rb +1 -1
  9. data/lib/llm/active_record/acts_as_llm.rb +6 -6
  10. data/lib/llm/agent.rb +136 -27
  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 +31 -37
  16. data/lib/llm/contract.rb +4 -25
  17. data/lib/llm/function/array.rb +18 -17
  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 +2 -2
  25. data/lib/llm/function/fork/task.rb +19 -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 +9 -3
  29. data/lib/llm/function/ractor/mailbox.rb +2 -0
  30. data/lib/llm/function/ractor/task.rb +23 -15
  31. data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
  32. data/lib/llm/function/sequential/task.rb +49 -0
  33. data/lib/llm/function/task.rb +25 -48
  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.rb +54 -65
  37. data/lib/llm/loop_guard.rb +1 -2
  38. data/lib/llm/mcp.rb +22 -0
  39. data/lib/llm/object.rb +2 -1
  40. data/lib/llm/provider.rb +6 -3
  41. data/lib/llm/providers/anthropic.rb +1 -1
  42. data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
  43. data/lib/llm/providers/google.rb +2 -2
  44. data/lib/llm/providers/mistral.rb +1 -1
  45. data/lib/llm/providers/ollama.rb +1 -1
  46. data/lib/llm/providers/openai/responses.rb +1 -1
  47. data/lib/llm/providers/openai.rb +1 -1
  48. data/lib/llm/repl/{transcript.rb → buffer.rb} +34 -21
  49. data/lib/llm/repl/command.rb +47 -13
  50. data/lib/llm/repl/commands/compact.rb +33 -0
  51. data/lib/llm/repl/commands/help.rb +3 -5
  52. data/lib/llm/repl/input.rb +80 -15
  53. data/lib/llm/repl/markdown/table.rb +80 -0
  54. data/lib/llm/repl/markdown.rb +33 -3
  55. data/lib/llm/repl/node.rb +37 -0
  56. data/lib/llm/repl/status.rb +4 -4
  57. data/lib/llm/repl/stream.rb +12 -5
  58. data/lib/llm/repl/walker.rb +46 -0
  59. data/lib/llm/repl/window.rb +31 -32
  60. data/lib/llm/repl.rb +70 -38
  61. data/lib/llm/response.rb +10 -0
  62. data/lib/llm/schema/leaf.rb +5 -0
  63. data/lib/llm/schema/object.rb +11 -5
  64. data/lib/llm/sequel/plugin.rb +6 -6
  65. data/lib/llm/skill.rb +20 -4
  66. data/lib/llm/stream.rb +24 -17
  67. data/lib/llm/tool.rb +20 -4
  68. data/lib/llm/tools/chdir.rb +0 -2
  69. data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
  70. data/lib/llm/tools/git.rb +11 -4
  71. data/lib/llm/tools/mkdir.rb +4 -1
  72. data/lib/llm/tools/pwd.rb +0 -2
  73. data/lib/llm/tools/read_file.rb +0 -2
  74. data/lib/llm/tools/rg.rb +11 -4
  75. data/lib/llm/tools/ruby.rb +46 -0
  76. data/lib/llm/tools/shell.rb +11 -4
  77. data/lib/llm/tools/utils.rb +31 -0
  78. data/lib/llm/tracer/pretty_logger.rb +127 -0
  79. data/lib/llm/tracer.rb +1 -0
  80. data/lib/llm/version.rb +1 -1
  81. data/lib/llm.rb +25 -5
  82. data/llm.gemspec +11 -5
  83. data/resources/deepdive.md +45 -1198
  84. metadata +39 -17
  85. data/lib/llm/function/call_task.rb +0 -46
  86. data/lib/llm/function/fiber_group.rb +0 -105
  87. data/lib/llm/function/task_group.rb +0 -97
  88. data/lib/llm/function/thread_group.rb +0 -102
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::Ruby LLM::Tool::Ruby} class implements
6
+ # a tool that can execute an arbitrary string of Ruby code
7
+ # and return the result. It uses `test-cmd.rb` under the
8
+ # hood for process management.
9
+ class Ruby < self
10
+ require_relative "utils"
11
+ include Utils
12
+
13
+ name "ruby"
14
+ description "runs a string of ruby code"
15
+ parameter :code, String, "a string of ruby code"
16
+ parameter :timeout, Integer, "maximum runtime before timeout"
17
+ required %i[code]
18
+ defaults timeout: 15
19
+
20
+ ##
21
+ # @param [String] code
22
+ # Ruby code
23
+ # @param [Integer] timeout
24
+ # Runtime timeout
25
+ def call(code:, timeout: 15)
26
+ command = spawn(code:)
27
+ wait(command:, timeout:)
28
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
29
+ rescue LLM::Interrupt
30
+ command.kill! if command&.running?
31
+ raise
32
+ end
33
+
34
+ private
35
+
36
+ def spawn(code:)
37
+ Command
38
+ .new(RbConfig.ruby)
39
+ .argv("-e", code)
40
+ end
41
+
42
+ require "rbconfig"
43
+ LLM.require "test-cmd.rb", "~> 1.1"
44
+ Command = Test::Cmd
45
+ end
46
+ 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,9 +27,13 @@ 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}
34
+ rescue LLM::Interrupt
35
+ command.kill! if command&.running?
36
+ raise
30
37
  end
31
38
 
32
39
  private
@@ -42,7 +49,7 @@ class LLM::Tool
42
49
  .spawn
43
50
  end
44
51
 
45
- LLM.require "test-cmd.rb"
52
+ LLM.require "test-cmd.rb", "~> 1.1"
46
53
  Command = Test::Cmd
47
54
  end
48
55
  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
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # The {LLM::Tracer::PrettyLogger LLM::Tracer::PrettyLogger} class
6
+ # writes human-readable request and tool call logs to a console
7
+ # or file. Each event is a single line with the relevant context
8
+ # inline, no structured JSON.
9
+ #
10
+ # @example
11
+ # llm = LLM.openai(key: ENV["KEY"])
12
+ # llm.tracer = LLM::Tracer::PrettyLogger.new(llm)
13
+ #
14
+ # @example Writing to a file
15
+ # llm.tracer = LLM::Tracer::PrettyLogger.new(llm, io: File.open("log.txt", "a"))
16
+ class Tracer::PrettyLogger < Tracer
17
+ ##
18
+ # @param (see LLM::Tracer#initialize)
19
+ def initialize(provider, options = {})
20
+ super
21
+ setup!(**options)
22
+ end
23
+
24
+ ##
25
+ # @param (see LLM::Tracer#on_request_start)
26
+ # @return [void]
27
+ def on_request_start(operation:, model: nil, **)
28
+ @start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ name = operation == "chat" ? "chat" : operation
30
+ @io.puts "#{timestamp} #{provider_name} #{name} (#{model || "default"})"
31
+ end
32
+
33
+ ##
34
+ # @param (see LLM::Tracer#on_request_finish)
35
+ # @return [void]
36
+ def on_request_finish(operation:, res:, model: nil, **)
37
+ elapsed = @start ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start).round(2) : nil
38
+ tokens = format_tokens(res)
39
+ name = operation == "chat" ? "chat" : operation
40
+ parts = ["#{timestamp} #{provider_name} #{name} done"]
41
+ parts << tokens if tokens
42
+ parts << "#{elapsed}s" if elapsed
43
+ @io.puts parts.join(", ")
44
+ end
45
+
46
+ ##
47
+ # @param (see LLM::Tracer#on_request_error)
48
+ # @return [void]
49
+ def on_request_error(ex:, **)
50
+ @io.puts "#{timestamp} #{provider_name} error #{ex.class}: #{ex.message}"
51
+ end
52
+
53
+ ##
54
+ # @param (see LLM::Tracer#on_tool_start)
55
+ # @return [void]
56
+ def on_tool_start(id:, name:, arguments:, **)
57
+ @io.puts "#{timestamp} #{name}(#{format_arguments(arguments)})"
58
+ end
59
+
60
+ ##
61
+ # @param (see LLM::Tracer#on_tool_finish)
62
+ # @return [void]
63
+ def on_tool_finish(result:, **)
64
+ @io.puts "#{timestamp} #{result.name} -> #{format_value(result.value)}"
65
+ end
66
+
67
+ ##
68
+ # @param (see LLM::Tracer#on_tool_error)
69
+ # @return [void]
70
+ def on_tool_error(ex:, **)
71
+ @io.puts "#{timestamp} tool error #{ex.class}: #{ex.message}"
72
+ end
73
+
74
+ private
75
+
76
+ def setup!(io: $stderr)
77
+ @io = io
78
+ @start = nil
79
+ end
80
+
81
+ def timestamp
82
+ Time.now.strftime("%H:%M:%S")
83
+ end
84
+
85
+ def format_tokens(res)
86
+ usage = res.usage
87
+ if usage.input_tokens and usage.output_tokens
88
+ "in=#{usage.input_tokens} out=#{usage.output_tokens}"
89
+ elsif usage.input_tokens
90
+ "in=#{usage.input_tokens}"
91
+ elsif usage.output_tokens
92
+ "out=#{usage.output_tokens}"
93
+ end
94
+ end
95
+
96
+ def format_arguments(args, max: 50)
97
+ return "" unless args
98
+ case args
99
+ when Hash, LLM::Object
100
+ result = args.map { |k, v| "#{k}: #{format_value(v)}" }.join(", ")
101
+ when Array
102
+ result = args.map { |v| format_value(v) }.join(", ")
103
+ else
104
+ result = args.inspect
105
+ end
106
+ result.size > max ? "#{result[0...max - 1]}..." : result
107
+ end
108
+
109
+ def format_value(value, max: 18)
110
+ case value
111
+ when String
112
+ value.size > max ? "#{value[0...max]}...".inspect : value.inspect
113
+ when Array
114
+ items = value.take(2).map { format_value(_1, max: 10) }
115
+ items << "..." if value.size > 2
116
+ "[#{items.join(", ")}]"
117
+ when Hash
118
+ "{...}"
119
+ when nil
120
+ "nil"
121
+ else
122
+ str = value.inspect
123
+ str.size > max ? "#{str[0...max]}..." : str
124
+ end
125
+ end
126
+ end
127
+ end
data/lib/llm/tracer.rb CHANGED
@@ -12,6 +12,7 @@ module LLM
12
12
  require_relative "tracer/logger"
13
13
  require_relative "tracer/telemetry"
14
14
  require_relative "tracer/null"
15
+ require_relative "tracer/pretty_logger"
15
16
 
16
17
  ##
17
18
  # @return [LLM::Provider]
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.6.0"
4
+ VERSION = "13.1.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
@@ -12,11 +12,11 @@ Gem::Specification.new do |spec|
12
12
  spec.description = <<~DESCRIPTION
13
13
  llm.rb is an advanced runtime for building capable AI applications
14
14
  on CRuby. By default it has zero runtime dependencies although certain
15
- functionality &ndash; such as ActiveRecord support &ndash; require
15
+ functionality (such as ActiveRecord support) 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/"
@@ -30,15 +30,21 @@ DESCRIPTION
30
30
  "lib/*.rb", "lib/**/*.rb",
31
31
  "data/*.json", "CHANGELOG.md",
32
32
  "resources/deepdive.md",
33
- "llm.gemspec"
33
+ "llm.gemspec", "bin/llm.rb"
34
34
  ]
35
+ spec.executables = ["llm.rb"]
35
36
  spec.require_paths = ["lib"]
37
+ spec.post_install_message = "\n" \
38
+ "Learn more about llm.rb by reading the deepdive:" \
39
+ "\n" \
40
+ "https://r.uby.dev/llm/deepdive" \
41
+ "\n\n"
36
42
 
37
43
  spec.add_development_dependency "webmock", "~> 3.24.0"
38
44
  spec.add_development_dependency "yard", "~> 0.9.37"
39
45
  spec.add_development_dependency "redcarpet", "~> 3.6"
40
46
  spec.add_development_dependency "webrick", "~> 1.8"
41
- spec.add_development_dependency "test-cmd.rb", "~> 0.12.0"
47
+ spec.add_development_dependency "test-cmd.rb", "~> 1.1.0"
42
48
  spec.add_development_dependency "rake", "~> 13.0"
43
49
  spec.add_development_dependency "rspec", "~> 3.0"
44
50
  spec.add_development_dependency "standard", "~> 1.50"
@@ -50,7 +56,7 @@ DESCRIPTION
50
56
  spec.add_development_dependency "activerecord", "~> 8.0"
51
57
  spec.add_development_dependency "sequel", "~> 5.0"
52
58
  spec.add_development_dependency "sqlite3", "~> 2.0"
53
- spec.add_development_dependency "xchan.rb", "~> 0.20"
59
+ spec.add_development_dependency "xchan.rb", "~> 0.22"
54
60
  spec.add_development_dependency "pg", "~> 1.5"
55
61
  spec.add_development_dependency "irb", "~> 1.18"
56
62
  end