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
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/transcript"
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
- @path = path
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(@agent)
41
- @transcript = Transcript.new
42
- @input = Input.new(@agent, height: 3)
43
- @window = Window.new(@status, @transcript, @input)
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
- transcript.write(chars, attrs)
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] chars
102
+ # @param [String] user
103
+ # @param [String] content
90
104
  # @return [void]
91
- def markdown(chars)
92
- transcript.markdown(chars)
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
- in [:error, String => who, String => text]
146
- write(who, Curses::A_BOLD)
147
- write(text)
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
- write("user: ", Curses::A_BOLD)
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.value = args[parameter.index]
225
+ kwargs[parameter.name] = args[parameter.index]
187
226
  end
188
227
  end
189
- [:command, command.new(self), parameters.transform_values(&:value)]
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
- transcript.start
207
- stream.empty!
245
+ buffer.open
246
+ stream.clear
208
247
  when :stream
209
- transcript.markdown(value, method: :replace)
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
- write("\n\n")
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
- write("\n\nagent: ", Curses::A_BOLD)
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
- write("\nerror: #{value.message}\n", Curses::A_BOLD)
228
- transcript.finish
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
 
@@ -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/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
- path = ::File.join(@path, "SKILL.md")
67
- parse(::File.read(path))
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 from its parent
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
- tools == "inherit" ? [true, []] : raise_invalid_error!(tools)
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
- # @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
@@ -2,11 +2,11 @@
2
2
 
3
3
  class LLM::Tool
4
4
  ##
5
- # The {LLM::Tool::SwapText LLM::Tool::SwapText} class
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 SwapText < self
9
- name "swap-text"
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: 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}
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
@@ -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
@@ -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,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