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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +571 -13
- data/LICENSE +21 -93
- data/README.md +183 -167
- data/bin/llm.rb +124 -0
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +136 -27
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +31 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +18 -17
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +2 -2
- data/lib/llm/function/fork/task.rb +19 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +9 -3
- data/lib/llm/function/ractor/mailbox.rb +2 -0
- data/lib/llm/function/ractor/task.rb +23 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -48
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function.rb +54 -65
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/providers/mistral.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/repl/{transcript.rb → buffer.rb} +34 -21
- data/lib/llm/repl/command.rb +47 -13
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +80 -0
- data/lib/llm/repl/markdown.rb +33 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +12 -5
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +70 -38
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +11 -4
- data/lib/llm/tools/mkdir.rb +4 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +11 -4
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +11 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/tracer/pretty_logger.rb +127 -0
- data/lib/llm/tracer.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +11 -5
- data/resources/deepdive.md +45 -1198
- metadata +39 -17
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- 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
|
data/lib/llm/tools/shell.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::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:
|
|
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
data/lib/llm/version.rb
CHANGED
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
|
-
# @
|
|
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}
|
|
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
|
|
15
|
+
functionality (such as ActiveRecord support) require
|
|
16
16
|
optional dependencies that are opt-in.
|
|
17
17
|
DESCRIPTION
|
|
18
18
|
|
|
19
|
-
spec.license = "
|
|
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", "~>
|
|
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.
|
|
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
|