llm.rb 12.6.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +387 -0
- data/LICENSE +21 -93
- data/README.md +46 -155
- 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 +62 -23
- 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 +15 -14
- 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 -64
- 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/google.rb +2 -2
- data/lib/llm/repl/command.rb +35 -8
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +76 -0
- data/lib/llm/repl/markdown.rb +31 -1
- data/lib/llm/repl/status.rb +1 -1
- data/lib/llm/repl/stream.rb +10 -3
- data/lib/llm/repl/transcript.rb +1 -1
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl.rb +18 -12
- 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/stream.rb +24 -17
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/git.rb +8 -4
- data/lib/llm/tools/mkdir.rb +1 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +8 -4
- data/lib/llm/tools/shell.rb +8 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +3 -3
- data/resources/deepdive.md +645 -58
- metadata +24 -13
- 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,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl::Markdown
|
|
4
|
+
##
|
|
5
|
+
# Renders Kramdown `:table` nodes as aligned columns.
|
|
6
|
+
module Table
|
|
7
|
+
##
|
|
8
|
+
# Renders a table node by collecting all cells first to
|
|
9
|
+
# compute column widths, then emitting each row with
|
|
10
|
+
# padded text.
|
|
11
|
+
def walk_table(node, attrs)
|
|
12
|
+
rows = collect_rows(node, attrs)
|
|
13
|
+
return if rows.empty?
|
|
14
|
+
widths = column_widths(rows)
|
|
15
|
+
rows.each do |row|
|
|
16
|
+
emit("| ", attrs)
|
|
17
|
+
row.each_with_index do |chunks, i|
|
|
18
|
+
text = chunks.map { _1[:text] }.join
|
|
19
|
+
width = widths[i]
|
|
20
|
+
chunks.each { |c| emit(c[:text].ljust(width), c[:attrs]) }
|
|
21
|
+
emit(" | ", attrs) unless i == row.size - 1
|
|
22
|
+
end
|
|
23
|
+
emit(" |", attrs)
|
|
24
|
+
emit("\n", attrs)
|
|
25
|
+
end
|
|
26
|
+
emit("\n", attrs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def collect_rows(node, attrs)
|
|
32
|
+
node.children.each_with_object([]) do |section, rows|
|
|
33
|
+
next unless [:thead, :tbody].include?(section.type)
|
|
34
|
+
section.children.each do |tr|
|
|
35
|
+
next unless tr.type == :tr
|
|
36
|
+
cells = tr.children.filter_map do |td|
|
|
37
|
+
next unless [:td, :th].include?(td.type)
|
|
38
|
+
collect_chunks(td, attrs)
|
|
39
|
+
end
|
|
40
|
+
rows << cells
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def collect_chunks(node, attrs)
|
|
46
|
+
[].tap do |chunks|
|
|
47
|
+
walk_collect(node, attrs, chunks)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def walk_collect(node, attrs, chunks)
|
|
52
|
+
case node.type
|
|
53
|
+
when :text
|
|
54
|
+
chunks << {text: node.value.to_s, attrs:}
|
|
55
|
+
when :strong
|
|
56
|
+
node.children.each { walk_collect(_1, Curses::A_BOLD, chunks) }
|
|
57
|
+
when :em
|
|
58
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
59
|
+
when :codespan
|
|
60
|
+
chunks << {text: node.value, attrs: Curses::A_REVERSE}
|
|
61
|
+
when :a
|
|
62
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
63
|
+
else
|
|
64
|
+
node.children.each { walk_collect(_1, attrs, chunks) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def column_widths(rows)
|
|
69
|
+
return [] if rows.empty?
|
|
70
|
+
cols = rows.first.size
|
|
71
|
+
(0...cols).map do |i|
|
|
72
|
+
rows.map { |r| r[i].map { _1[:text] }.join.length }.max
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/llm/repl/markdown.rb
CHANGED
|
@@ -8,11 +8,16 @@ class LLM::Repl
|
|
|
8
8
|
# of text, and also optional style information for
|
|
9
9
|
# that text (eg bold, underscore, ...)
|
|
10
10
|
class Markdown
|
|
11
|
+
require_relative "markdown/table"
|
|
12
|
+
include Table
|
|
13
|
+
|
|
11
14
|
##
|
|
12
15
|
# @param [String] text
|
|
16
|
+
# @param [Integer] width
|
|
13
17
|
# @return [LLM::Repl::Markdown]
|
|
14
|
-
def initialize(text)
|
|
18
|
+
def initialize(text, width)
|
|
15
19
|
@doc = Kramdown::Document.new(text)
|
|
20
|
+
@width = width
|
|
16
21
|
@ast = []
|
|
17
22
|
end
|
|
18
23
|
|
|
@@ -73,6 +78,31 @@ class LLM::Repl
|
|
|
73
78
|
emit("\n\n", attrs)
|
|
74
79
|
when :br
|
|
75
80
|
emit("\n", attrs)
|
|
81
|
+
when :ul, :ol
|
|
82
|
+
node.children.each { walk(_1, attrs) }
|
|
83
|
+
when :li
|
|
84
|
+
node.children.each { walk(_1, attrs) }
|
|
85
|
+
when :table
|
|
86
|
+
walk_table(node, attrs)
|
|
87
|
+
when :thead, :tbody
|
|
88
|
+
node.children.each { walk(_1, attrs) }
|
|
89
|
+
when :tr
|
|
90
|
+
emit("| ", attrs)
|
|
91
|
+
node.children.each { walk(_1, attrs) }
|
|
92
|
+
emit("\n", attrs)
|
|
93
|
+
when :td, :th
|
|
94
|
+
node.children.each { walk(_1, attrs) }
|
|
95
|
+
emit(" | ", attrs)
|
|
96
|
+
when :blockquote
|
|
97
|
+
emit("> ", attrs)
|
|
98
|
+
node.children.each { walk(_1, attrs) }
|
|
99
|
+
when :hr
|
|
100
|
+
emit("─" * @width, attrs)
|
|
101
|
+
emit("\n\n", attrs)
|
|
102
|
+
when :a
|
|
103
|
+
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
104
|
+
when :img
|
|
105
|
+
emit("[image: #{node.attr['alt']}]", attrs)
|
|
76
106
|
else
|
|
77
107
|
node.children.each { walk(_1, attrs) }
|
|
78
108
|
end
|
data/lib/llm/repl/status.rb
CHANGED
data/lib/llm/repl/stream.rb
CHANGED
|
@@ -60,9 +60,16 @@ class LLM::Repl
|
|
|
60
60
|
# @param [Integer] max
|
|
61
61
|
# @return [String]
|
|
62
62
|
def format_args(tool, max: 50)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
##
|
|
64
|
+
# 'tool.arguments' might be returned
|
|
65
|
+
# (by the model) in a different order
|
|
66
|
+
# than the tool definition - this code
|
|
67
|
+
# handles re-sorting.
|
|
68
|
+
args = tool.arguments
|
|
69
|
+
props = tool.params.properties.keys
|
|
70
|
+
props = props.sort_by { tool.params.properties[_1].index }
|
|
71
|
+
props = props.filter_map { args[_1] ? "#{_1}: #{format_value(args[_1])}" : nil }
|
|
72
|
+
result = props.join(", ")
|
|
66
73
|
result.size > max ? "#{result[0...max - 1]}…" : result
|
|
67
74
|
end
|
|
68
75
|
|
data/lib/llm/repl/transcript.rb
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# Walks through an array from the last element backward,
|
|
6
|
+
# one step at a time. {#next} moves forward toward the end.
|
|
7
|
+
# Both methods clamp at the array boundaries.
|
|
8
|
+
class Walker
|
|
9
|
+
##
|
|
10
|
+
# Sets the cursor position
|
|
11
|
+
attr_writer :cursor
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @param [Array] items
|
|
15
|
+
def initialize(items)
|
|
16
|
+
@items = items
|
|
17
|
+
@cursor = items.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [Object, nil]
|
|
22
|
+
def next
|
|
23
|
+
if @items.empty?
|
|
24
|
+
nil
|
|
25
|
+
elsif @cursor >= @items.size - 1
|
|
26
|
+
@items[@cursor]
|
|
27
|
+
else
|
|
28
|
+
@cursor += 1
|
|
29
|
+
@items[@cursor]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [Object, nil]
|
|
35
|
+
def prev
|
|
36
|
+
if @items.empty?
|
|
37
|
+
nil
|
|
38
|
+
elsif @cursor <= 0
|
|
39
|
+
@items[@cursor]
|
|
40
|
+
else
|
|
41
|
+
@cursor -= 1
|
|
42
|
+
@items[@cursor]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -23,9 +23,17 @@ module LLM
|
|
|
23
23
|
require_relative "repl/stream"
|
|
24
24
|
require_relative "repl/markdown"
|
|
25
25
|
require_relative "repl/command"
|
|
26
|
+
require_relative "repl/walker"
|
|
27
|
+
|
|
28
|
+
attr_reader :agent, :provider, :stream,
|
|
29
|
+
:status, :transcript, :input,
|
|
30
|
+
:window, :tools, :thread,
|
|
31
|
+
:name, :path
|
|
26
32
|
|
|
27
33
|
##
|
|
28
34
|
# @param [LLM::Agent] agent
|
|
35
|
+
# @param [String, nil] name
|
|
36
|
+
# The agent's name (optional)
|
|
29
37
|
# @param [String, nil] path
|
|
30
38
|
# The path where to maintain runtime state
|
|
31
39
|
# @param [Array<LLM::Tool>] tools
|
|
@@ -33,13 +41,14 @@ module LLM
|
|
|
33
41
|
# @param [Array<String>] skills
|
|
34
42
|
# Zero or more skills
|
|
35
43
|
# @return [LLM::Repl]
|
|
36
|
-
def initialize(agent:, tools: [], skills: [], path: nil)
|
|
37
|
-
@path
|
|
44
|
+
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
45
|
+
@path = path
|
|
46
|
+
@name = name || "agent"
|
|
38
47
|
@agent = configure(agent:, path:)
|
|
39
48
|
@provider = agent.llm.name
|
|
40
49
|
@status = Status.new(@agent)
|
|
41
50
|
@transcript = Transcript.new
|
|
42
|
-
@input = Input.new(
|
|
51
|
+
@input = Input.new(self, height: 3)
|
|
43
52
|
@window = Window.new(@status, @transcript, @input)
|
|
44
53
|
@thread = nil
|
|
45
54
|
@queue = Queue.new
|
|
@@ -150,7 +159,7 @@ module LLM
|
|
|
150
159
|
status.text = thinking_text
|
|
151
160
|
write("user: ", Curses::A_BOLD)
|
|
152
161
|
markdown(text)
|
|
153
|
-
write("\
|
|
162
|
+
write("\n#{name}: ", Curses::A_BOLD)
|
|
154
163
|
@thread = Thread.new do
|
|
155
164
|
@queue << [:start]
|
|
156
165
|
agent.talk(text, tools:, stream:)
|
|
@@ -181,12 +190,14 @@ module LLM
|
|
|
181
190
|
"command(#{command.name}): ",
|
|
182
191
|
"too few arguments: expected #{reqc} but got #{args.size}\n\n"]
|
|
183
192
|
else
|
|
193
|
+
parameters = parameters.sort_by(&:index).to_h
|
|
194
|
+
kwargs = {}
|
|
184
195
|
parameters.each_value do |parameter|
|
|
185
196
|
if args[parameter.index]
|
|
186
|
-
parameter.
|
|
197
|
+
kwargs[parameter.name] = args[parameter.index]
|
|
187
198
|
end
|
|
188
199
|
end
|
|
189
|
-
[:command, command.new(self),
|
|
200
|
+
[:command, command.new(self), kwargs]
|
|
190
201
|
end
|
|
191
202
|
else
|
|
192
203
|
[:input, text]
|
|
@@ -216,7 +227,7 @@ module LLM
|
|
|
216
227
|
@thread = nil
|
|
217
228
|
when :cancel
|
|
218
229
|
status.text = "Idle"
|
|
219
|
-
write("\n\
|
|
230
|
+
write("\n\n#{name}: ", Curses::A_BOLD)
|
|
220
231
|
write("request cancelled!")
|
|
221
232
|
write("\n\n")
|
|
222
233
|
transcript.finish
|
|
@@ -246,11 +257,6 @@ module LLM
|
|
|
246
257
|
File.readable?(path)
|
|
247
258
|
end
|
|
248
259
|
|
|
249
|
-
attr_reader :agent, :provider, :stream,
|
|
250
|
-
:status, :transcript, :input,
|
|
251
|
-
:window, :tools, :thread,
|
|
252
|
-
:path
|
|
253
|
-
|
|
254
260
|
File = ::File
|
|
255
261
|
private_constant :File
|
|
256
262
|
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/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
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:
|
|
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
|
data/lib/llm/tools/mkdir.rb
CHANGED
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,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
|