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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +387 -0
  3. data/LICENSE +21 -93
  4. data/README.md +46 -155
  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_llm.rb +6 -6
  9. data/lib/llm/agent.rb +62 -23
  10. data/lib/llm/buffer.rb +85 -3
  11. data/lib/llm/compactor/null.rb +19 -0
  12. data/lib/llm/compactor/truncate.rb +80 -0
  13. data/lib/llm/compactor.rb +42 -124
  14. data/lib/llm/context.rb +31 -37
  15. data/lib/llm/contract.rb +4 -25
  16. data/lib/llm/function/array.rb +15 -14
  17. data/lib/llm/function/async/group.rb +54 -0
  18. data/lib/llm/function/async/reactor.rb +48 -0
  19. data/lib/llm/function/async/task.rb +83 -0
  20. data/lib/llm/function/fiber/group.rb +46 -0
  21. data/lib/llm/function/fiber/task.rb +62 -0
  22. data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
  23. data/lib/llm/function/fork/job.rb +2 -2
  24. data/lib/llm/function/fork/task.rb +19 -10
  25. data/lib/llm/function/group.rb +40 -0
  26. data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
  27. data/lib/llm/function/ractor/job.rb +9 -3
  28. data/lib/llm/function/ractor/mailbox.rb +2 -0
  29. data/lib/llm/function/ractor/task.rb +23 -15
  30. data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
  31. data/lib/llm/function/sequential/task.rb +49 -0
  32. data/lib/llm/function/task.rb +25 -48
  33. data/lib/llm/function/thread/group.rb +46 -0
  34. data/lib/llm/function/thread/task.rb +60 -0
  35. data/lib/llm/function.rb +54 -64
  36. data/lib/llm/loop_guard.rb +1 -2
  37. data/lib/llm/mcp.rb +22 -0
  38. data/lib/llm/object.rb +2 -1
  39. data/lib/llm/provider.rb +6 -3
  40. data/lib/llm/providers/google.rb +2 -2
  41. data/lib/llm/repl/command.rb +35 -8
  42. data/lib/llm/repl/commands/compact.rb +33 -0
  43. data/lib/llm/repl/input.rb +80 -15
  44. data/lib/llm/repl/markdown/table.rb +76 -0
  45. data/lib/llm/repl/markdown.rb +31 -1
  46. data/lib/llm/repl/status.rb +1 -1
  47. data/lib/llm/repl/stream.rb +10 -3
  48. data/lib/llm/repl/transcript.rb +1 -1
  49. data/lib/llm/repl/walker.rb +46 -0
  50. data/lib/llm/repl.rb +18 -12
  51. data/lib/llm/response.rb +10 -0
  52. data/lib/llm/schema/leaf.rb +5 -0
  53. data/lib/llm/schema/object.rb +11 -5
  54. data/lib/llm/sequel/plugin.rb +6 -6
  55. data/lib/llm/stream.rb +24 -17
  56. data/lib/llm/tool.rb +20 -4
  57. data/lib/llm/tools/chdir.rb +0 -2
  58. data/lib/llm/tools/git.rb +8 -4
  59. data/lib/llm/tools/mkdir.rb +1 -1
  60. data/lib/llm/tools/pwd.rb +0 -2
  61. data/lib/llm/tools/read_file.rb +0 -2
  62. data/lib/llm/tools/rg.rb +8 -4
  63. data/lib/llm/tools/shell.rb +8 -4
  64. data/lib/llm/tools/utils.rb +31 -0
  65. data/lib/llm/version.rb +1 -1
  66. data/lib/llm.rb +25 -5
  67. data/llm.gemspec +3 -3
  68. data/resources/deepdive.md +645 -58
  69. metadata +24 -13
  70. data/lib/llm/function/call_task.rb +0 -46
  71. data/lib/llm/function/fiber_group.rb +0 -105
  72. data/lib/llm/function/task_group.rb +0 -97
  73. 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
@@ -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
@@ -7,7 +7,7 @@ class LLM::Repl
7
7
  # @api private
8
8
  class Status
9
9
  ##
10
- # @param [String, Symbol] provider
10
+ # @param [LLM::Agent] agent
11
11
  # @return [LLM::Repl::Status]
12
12
  def initialize(agent)
13
13
  @agent = agent
@@ -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
- args = tool.arguments
64
- pairs = args.to_h.map { "#{_1}: #{format_value(_2)}" }
65
- result = pairs.join(", ")
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
 
@@ -42,7 +42,7 @@ class LLM::Repl
42
42
  # @param [Symbol] method
43
43
  # @return [void]
44
44
  def markdown(chars, method: :append)
45
- chunks = LLM::Repl::Markdown.new(chars).ast
45
+ chunks = LLM::Repl::Markdown.new(chars, WIDTH).ast
46
46
  self.method(method).call(chunks)
47
47
  end
48
48
 
@@ -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 = 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(@agent, height: 3)
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("\nagent: ", Curses::A_BOLD)
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.value = args[parameter.index]
197
+ kwargs[parameter.name] = args[parameter.index]
187
198
  end
188
199
  end
189
- [:command, command.new(self), parameters.transform_values(&:value)]
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\nagent: ", Curses::A_BOLD)
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
 
@@ -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
  ##
@@ -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 = 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.to_s]
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
- properties[key.to_s] = val
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
- @properties.merge!(other.properties)
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 { _2.required? ? _1 : nil }
74
+ @properties.filter_map { _2.required? ? _1 : nil }
69
75
  end
70
76
  end
71
77
  end
@@ -204,17 +204,17 @@ module LLM::Sequel
204
204
  end
205
205
 
206
206
  ##
207
- # @see LLM::Context#functions
207
+ # @see LLM::Context#pending_functions
208
208
  # @return [Array<LLM::Function>]
209
- def functions
210
- ctx.functions
209
+ def pending_functions
210
+ ctx.pending_functions
211
211
  end
212
212
 
213
213
  ##
214
- # @see LLM::Context#functions?
214
+ # @see LLM::Context#pending_functions?
215
215
  # @return [Boolean]
216
- def functions?
217
- ctx.functions?
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
- # @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
 
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