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
data/lib/llm/repl.rb
CHANGED
|
@@ -17,15 +17,24 @@ module LLM
|
|
|
17
17
|
class Repl
|
|
18
18
|
require_relative "repl/window"
|
|
19
19
|
require_relative "repl/status"
|
|
20
|
-
require_relative "repl/
|
|
20
|
+
require_relative "repl/buffer"
|
|
21
21
|
require_relative "repl/input"
|
|
22
22
|
require_relative "repl/bar"
|
|
23
23
|
require_relative "repl/stream"
|
|
24
|
+
require_relative "repl/node"
|
|
24
25
|
require_relative "repl/markdown"
|
|
25
26
|
require_relative "repl/command"
|
|
27
|
+
require_relative "repl/walker"
|
|
28
|
+
|
|
29
|
+
attr_reader :agent, :provider, :stream,
|
|
30
|
+
:status, :buffer, :input,
|
|
31
|
+
:window, :tools, :thread,
|
|
32
|
+
:name, :path, :width
|
|
26
33
|
|
|
27
34
|
##
|
|
28
35
|
# @param [LLM::Agent] agent
|
|
36
|
+
# @param [String, nil] name
|
|
37
|
+
# The agent's name (optional)
|
|
29
38
|
# @param [String, nil] path
|
|
30
39
|
# The path where to maintain runtime state
|
|
31
40
|
# @param [Array<LLM::Tool>] tools
|
|
@@ -33,14 +42,16 @@ module LLM
|
|
|
33
42
|
# @param [Array<String>] skills
|
|
34
43
|
# Zero or more skills
|
|
35
44
|
# @return [LLM::Repl]
|
|
36
|
-
def initialize(agent:, tools: [], skills: [], path: nil)
|
|
37
|
-
@
|
|
45
|
+
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
46
|
+
@width = 80
|
|
47
|
+
@path = path
|
|
48
|
+
@name = name || "agent"
|
|
38
49
|
@agent = configure(agent:, path:)
|
|
39
50
|
@provider = agent.llm.name
|
|
40
|
-
@status = Status.new(
|
|
41
|
-
@
|
|
42
|
-
@input = Input.new(
|
|
43
|
-
@window = Window.new(
|
|
51
|
+
@status = Status.new(self)
|
|
52
|
+
@buffer = Buffer.new(self)
|
|
53
|
+
@input = Input.new(self, height: 3)
|
|
54
|
+
@window = Window.new(self)
|
|
44
55
|
@thread = nil
|
|
45
56
|
@queue = Queue.new
|
|
46
57
|
@stream = Stream.new(self, @queue)
|
|
@@ -61,6 +72,7 @@ module LLM
|
|
|
61
72
|
def start
|
|
62
73
|
window.open do
|
|
63
74
|
catch(:exit) do
|
|
75
|
+
write tree(agent.messages)
|
|
64
76
|
loop do
|
|
65
77
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
78
|
case input.on_char(window, input.paste? ? window.read_paste : window.getch, now)
|
|
@@ -77,22 +89,32 @@ module LLM
|
|
|
77
89
|
end
|
|
78
90
|
|
|
79
91
|
##
|
|
80
|
-
# @param [String] chars
|
|
92
|
+
# @param [String, Array] chars
|
|
81
93
|
# @param [Object] attrs
|
|
94
|
+
# @param [Symbol] method (:append, :replace)
|
|
82
95
|
# @return [void]
|
|
83
|
-
def write(chars, attrs = nil)
|
|
84
|
-
|
|
96
|
+
def write(chars, attrs = nil, method: :append)
|
|
97
|
+
buffer.write(chars, attrs, method:)
|
|
85
98
|
window.redraw
|
|
86
99
|
end
|
|
87
100
|
|
|
88
101
|
##
|
|
89
|
-
# @param [String]
|
|
102
|
+
# @param [String] user
|
|
103
|
+
# @param [String] content
|
|
90
104
|
# @return [void]
|
|
91
|
-
def
|
|
92
|
-
|
|
105
|
+
def write_message(user, content, method: :append)
|
|
106
|
+
buffer.write_message(user, content, method:)
|
|
93
107
|
window.redraw
|
|
94
108
|
end
|
|
95
109
|
|
|
110
|
+
##
|
|
111
|
+
# Returns an AST
|
|
112
|
+
# @param [String] chars
|
|
113
|
+
# @return [Array<Node>]
|
|
114
|
+
def markdown(chars)
|
|
115
|
+
LLM::Repl::Markdown.new(chars, width).ast
|
|
116
|
+
end
|
|
117
|
+
|
|
96
118
|
##
|
|
97
119
|
# @param [String] value
|
|
98
120
|
# @return [void]
|
|
@@ -109,6 +131,22 @@ module LLM
|
|
|
109
131
|
|
|
110
132
|
private
|
|
111
133
|
|
|
134
|
+
##
|
|
135
|
+
# @param [LLM::Buffer] messages
|
|
136
|
+
# A message buffer
|
|
137
|
+
# @return [Array<Node>]
|
|
138
|
+
def tree(messages)
|
|
139
|
+
messages.flat_map do |message|
|
|
140
|
+
next if message.tool_call? || message.tool_return?
|
|
141
|
+
user = message.assistant? ? name : "user"
|
|
142
|
+
[
|
|
143
|
+
Node.new("#{user}: ", Curses::A_BOLD),
|
|
144
|
+
Node.new(message.content),
|
|
145
|
+
Node.new(user == name ? "\n\n" : "\n")
|
|
146
|
+
]
|
|
147
|
+
end.compact
|
|
148
|
+
end
|
|
149
|
+
|
|
112
150
|
##
|
|
113
151
|
# @param [LLM::Agent] agent
|
|
114
152
|
# An agent
|
|
@@ -142,15 +180,14 @@ module LLM
|
|
|
142
180
|
case on_text(text)
|
|
143
181
|
in [:command, Command => command, Hash => parameters]
|
|
144
182
|
command.call(**parameters)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
183
|
+
write("\n")
|
|
184
|
+
in [:error, String => user, String => text]
|
|
185
|
+
write_message(user, text)
|
|
186
|
+
write("\n")
|
|
148
187
|
in [:input, String => text]
|
|
149
188
|
window.scroll_to_bottom
|
|
150
189
|
status.text = thinking_text
|
|
151
|
-
|
|
152
|
-
markdown(text)
|
|
153
|
-
write("\nagent: ", Curses::A_BOLD)
|
|
190
|
+
write_message("user", markdown(text))
|
|
154
191
|
@thread = Thread.new do
|
|
155
192
|
@queue << [:start]
|
|
156
193
|
agent.talk(text, tools:, stream:)
|
|
@@ -181,12 +218,14 @@ module LLM
|
|
|
181
218
|
"command(#{command.name}): ",
|
|
182
219
|
"too few arguments: expected #{reqc} but got #{args.size}\n\n"]
|
|
183
220
|
else
|
|
221
|
+
parameters = parameters.sort_by(&:index).to_h
|
|
222
|
+
kwargs = {}
|
|
184
223
|
parameters.each_value do |parameter|
|
|
185
224
|
if args[parameter.index]
|
|
186
|
-
parameter.
|
|
225
|
+
kwargs[parameter.name] = args[parameter.index]
|
|
187
226
|
end
|
|
188
227
|
end
|
|
189
|
-
[:command, command.new(self),
|
|
228
|
+
[:command, command.new(self), kwargs]
|
|
190
229
|
end
|
|
191
230
|
else
|
|
192
231
|
[:input, text]
|
|
@@ -203,29 +242,27 @@ module LLM
|
|
|
203
242
|
type, value = @queue.pop(true)
|
|
204
243
|
case type
|
|
205
244
|
when :start
|
|
206
|
-
|
|
207
|
-
stream.
|
|
245
|
+
buffer.open
|
|
246
|
+
stream.clear
|
|
208
247
|
when :stream
|
|
209
|
-
|
|
248
|
+
write_message name, markdown(value), method: :replace
|
|
210
249
|
when :status
|
|
211
250
|
self.status = value
|
|
212
251
|
when :done
|
|
213
252
|
status.text = "idle"
|
|
214
|
-
|
|
215
|
-
transcript.finish
|
|
253
|
+
buffer.close
|
|
216
254
|
@thread = nil
|
|
255
|
+
write("\n")
|
|
217
256
|
when :cancel
|
|
257
|
+
buffer.close
|
|
218
258
|
status.text = "Idle"
|
|
219
|
-
|
|
220
|
-
write("request cancelled!")
|
|
221
|
-
write("\n\n")
|
|
222
|
-
transcript.finish
|
|
259
|
+
write_message(name, "Request cancelled")
|
|
223
260
|
@thread = nil
|
|
261
|
+
write("\n")
|
|
224
262
|
when :error
|
|
225
|
-
# Do this better
|
|
226
263
|
status.text = "error"
|
|
227
|
-
|
|
228
|
-
|
|
264
|
+
write_message(name, "(#{value.class}): #{value.message}")
|
|
265
|
+
buffer.close
|
|
229
266
|
@thread = nil
|
|
230
267
|
end
|
|
231
268
|
end
|
|
@@ -246,11 +283,6 @@ module LLM
|
|
|
246
283
|
File.readable?(path)
|
|
247
284
|
end
|
|
248
285
|
|
|
249
|
-
attr_reader :agent, :provider, :stream,
|
|
250
|
-
:status, :transcript, :input,
|
|
251
|
-
:window, :tools, :thread,
|
|
252
|
-
:path
|
|
253
|
-
|
|
254
286
|
File = ::File
|
|
255
287
|
private_constant :File
|
|
256
288
|
end
|
data/lib/llm/response.rb
CHANGED
|
@@ -10,12 +10,22 @@ module LLM
|
|
|
10
10
|
# handling can share one common surface without flattening away
|
|
11
11
|
# specialized behavior.
|
|
12
12
|
#
|
|
13
|
+
# @example Accessing response data
|
|
14
|
+
# res = agent.talk "Weather in Paris?", schema: Weather
|
|
15
|
+
# res.content # => raw text or structured data
|
|
16
|
+
# res.content! # => parsed JSON as LLM::Object
|
|
17
|
+
# res.usage # => token usage (input, output, cache, etc.)
|
|
18
|
+
# res.messages # => message history from this turn
|
|
19
|
+
#
|
|
13
20
|
# The normalized response keeps the transport response available
|
|
14
21
|
# through {#res}. When the default net/http transport is in use,
|
|
15
22
|
# {LLM::Transport::Response::HTTP
|
|
16
23
|
# LLM::Transport::Response::HTTP} keeps the
|
|
17
24
|
# original `Net::HTTPResponse` available through
|
|
18
25
|
# its own {LLM::Transport::Response::HTTP#res #res}.
|
|
26
|
+
#
|
|
27
|
+
# @see LLM::Context Responses are returned by Context#talk
|
|
28
|
+
# @see LLM::Agent Responses are returned by Agent#talk
|
|
19
29
|
class Response
|
|
20
30
|
require "json"
|
|
21
31
|
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -8,12 +8,17 @@ class LLM::Schema
|
|
|
8
8
|
# an example of how to create instances of {LLM::Schema::Leaf LLM::Schema::Leaf}
|
|
9
9
|
# through its subclasses.
|
|
10
10
|
class Leaf
|
|
11
|
+
##
|
|
12
|
+
# @return [Integer, nil]
|
|
13
|
+
attr_accessor :index
|
|
14
|
+
|
|
11
15
|
def initialize
|
|
12
16
|
@description = nil
|
|
13
17
|
@default = nil
|
|
14
18
|
@enum = nil
|
|
15
19
|
@required = nil
|
|
16
20
|
@const = nil
|
|
21
|
+
@index = nil
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
##
|
data/lib/llm/schema/object.rb
CHANGED
|
@@ -16,21 +16,22 @@ class LLM::Schema
|
|
|
16
16
|
# A hash of properties
|
|
17
17
|
# @return [LLM::Schema::Object]
|
|
18
18
|
def initialize(properties)
|
|
19
|
-
@properties =
|
|
19
|
+
@properties = LLM::Object.new
|
|
20
|
+
properties.each { set!(_1, _2) }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
##
|
|
23
24
|
# Get a property
|
|
24
25
|
# @return [LLM::Schema::Leaf]
|
|
25
26
|
def [](key)
|
|
26
|
-
properties[key
|
|
27
|
+
properties[key]
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
30
31
|
# Set a property
|
|
31
32
|
# @return [void]
|
|
32
33
|
def []=(key, val)
|
|
33
|
-
|
|
34
|
+
set!(key, val)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
##
|
|
@@ -46,7 +47,7 @@ class LLM::Schema
|
|
|
46
47
|
# Returns self
|
|
47
48
|
def merge!(other)
|
|
48
49
|
raise TypeError, "expected #{self.class} but got #{other.class}" unless self.class === other
|
|
49
|
-
|
|
50
|
+
other.properties.each { |key, val| self[key] = val }
|
|
50
51
|
self
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -64,8 +65,13 @@ class LLM::Schema
|
|
|
64
65
|
|
|
65
66
|
private
|
|
66
67
|
|
|
68
|
+
def set!(key, val)
|
|
69
|
+
val.index = @properties.size
|
|
70
|
+
properties[key] = val
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
def required_items
|
|
68
|
-
@properties.filter_map {
|
|
74
|
+
@properties.filter_map { _2.required? ? _1 : nil }
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
end
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -204,17 +204,17 @@ module LLM::Sequel
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
##
|
|
207
|
-
# @see LLM::Context#
|
|
207
|
+
# @see LLM::Context#pending_functions
|
|
208
208
|
# @return [Array<LLM::Function>]
|
|
209
|
-
def
|
|
210
|
-
ctx.
|
|
209
|
+
def pending_functions
|
|
210
|
+
ctx.pending_functions
|
|
211
211
|
end
|
|
212
212
|
|
|
213
213
|
##
|
|
214
|
-
# @see LLM::Context#
|
|
214
|
+
# @see LLM::Context#pending_functions?
|
|
215
215
|
# @return [Boolean]
|
|
216
|
-
def
|
|
217
|
-
ctx.
|
|
216
|
+
def pending_functions?
|
|
217
|
+
ctx.pending_functions?
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
##
|
data/lib/llm/skill.rb
CHANGED
|
@@ -7,6 +7,11 @@ module LLM
|
|
|
7
7
|
# Skills can expose themselves as normal {LLM::Tool LLM::Tool} classes through
|
|
8
8
|
# {#to_tool}. This keeps skills on the same execution path as local tools.
|
|
9
9
|
class Skill
|
|
10
|
+
##
|
|
11
|
+
# @api private
|
|
12
|
+
File = ::File
|
|
13
|
+
private_constant :File
|
|
14
|
+
|
|
10
15
|
##
|
|
11
16
|
# Load a skill from a directory.
|
|
12
17
|
# @param [String, Pathname] path
|
|
@@ -63,8 +68,14 @@ module LLM
|
|
|
63
68
|
# Load and parse the skill.
|
|
64
69
|
# @return [LLM::Skill]
|
|
65
70
|
def load!
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if File.file?(@path)
|
|
72
|
+
parse(File.read(@path))
|
|
73
|
+
elsif File.directory?(@path)
|
|
74
|
+
path = File.join(@path, "SKILL.md")
|
|
75
|
+
parse(File.read(path))
|
|
76
|
+
else
|
|
77
|
+
raise LLM::Error, "'#{@path}' is neither a file or directory"
|
|
78
|
+
end
|
|
68
79
|
self
|
|
69
80
|
end
|
|
70
81
|
|
|
@@ -103,7 +114,8 @@ module LLM
|
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
##
|
|
106
|
-
# Returns true when a skill should inherit tools
|
|
117
|
+
# Returns true when a skill should inherit tools
|
|
118
|
+
# from its parent
|
|
107
119
|
# @return [Boolean]
|
|
108
120
|
def inherit_tools?
|
|
109
121
|
@inherit_tools
|
|
@@ -138,7 +150,11 @@ module LLM
|
|
|
138
150
|
def parse_tools(tools)
|
|
139
151
|
case tools
|
|
140
152
|
when String
|
|
141
|
-
|
|
153
|
+
case tools
|
|
154
|
+
when "inherit" then [true, []]
|
|
155
|
+
when "all", "*" then [false, LLM::Tool.registry]
|
|
156
|
+
else raise_invalid_error!(tools)
|
|
157
|
+
end
|
|
142
158
|
when Array
|
|
143
159
|
[false, [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }]
|
|
144
160
|
when NilClass
|
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
|
-
#
|
|
113
|
-
#
|
|
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
|
-
#
|
|
132
|
-
#
|
|
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(
|
|
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(
|
|
182
|
+
def on_compaction_finish(compactor)
|
|
176
183
|
nil
|
|
177
184
|
end
|
|
178
185
|
|
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
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
data/lib/llm/tools/chdir.rb
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::Tool::
|
|
5
|
+
# The {LLM::Tool::EditFile LLM::Tool::EditFile} class
|
|
6
6
|
# implements a tool that can substitute one piece of
|
|
7
7
|
# text for another piece of text in a given file.
|
|
8
|
-
class
|
|
9
|
-
name "
|
|
8
|
+
class EditFile < self
|
|
9
|
+
name "edit-file"
|
|
10
10
|
description "Replace an exact snippet in a file"
|
|
11
11
|
parameter :path, String, "Path to file"
|
|
12
12
|
parameter :before, String, "Exact text to replace"
|
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,19 +7,28 @@ 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:
|
|
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}
|
|
29
|
+
rescue LLM::Interrupt
|
|
30
|
+
command.kill! if command&.running?
|
|
31
|
+
raise
|
|
25
32
|
end
|
|
26
33
|
|
|
27
34
|
private
|
|
@@ -34,7 +41,7 @@ class LLM::Tool
|
|
|
34
41
|
.spawn
|
|
35
42
|
end
|
|
36
43
|
|
|
37
|
-
LLM.require "test-cmd.rb"
|
|
44
|
+
LLM.require "test-cmd.rb", "~> 1.1"
|
|
38
45
|
Command = Test::Cmd
|
|
39
46
|
end
|
|
40
47
|
end
|
data/lib/llm/tools/mkdir.rb
CHANGED
|
@@ -15,6 +15,9 @@ class LLM::Tool
|
|
|
15
15
|
def call(path:)
|
|
16
16
|
command = spawn(path:)
|
|
17
17
|
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
18
|
+
rescue LLM::Interrupt
|
|
19
|
+
command.kill! if command&.running?
|
|
20
|
+
raise
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
private
|
|
@@ -26,7 +29,7 @@ class LLM::Tool
|
|
|
26
29
|
.spawn
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
LLM.require "test-cmd.rb"
|
|
32
|
+
LLM.require "test-cmd.rb", "~> 1.1"
|
|
30
33
|
Command = Test::Cmd
|
|
31
34
|
end
|
|
32
35
|
end
|
data/lib/llm/tools/pwd.rb
CHANGED
data/lib/llm/tools/read_file.rb
CHANGED
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,20 +7,29 @@ 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}
|
|
30
|
+
rescue LLM::Interrupt
|
|
31
|
+
command.kill! if command&.running?
|
|
32
|
+
raise
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
private
|
|
@@ -41,7 +48,7 @@ class LLM::Tool
|
|
|
41
48
|
.spawn
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
LLM.require "test-cmd.rb"
|
|
51
|
+
LLM.require "test-cmd.rb", "~> 1.1"
|
|
45
52
|
Command = Test::Cmd
|
|
46
53
|
end
|
|
47
54
|
end
|