llm.rb 12.0.0 → 12.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.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Git LLM::Tool::Git} class implements
8
+ # a tool that can perform a select number of git actions.
9
+ # The actions it can perform are read-only - at least for
10
+ # the time being.
11
+ class Git < self
12
+ name "git"
13
+ description "perform an action with git"
14
+ parameter :action, Enum["log", "diff", "commit", "checkout", "branch", "show"], "the git operation to perform"
15
+ parameter :arguments, Array[String], "one or more arguments for the git action"
16
+ required %i[action]
17
+
18
+ ##
19
+ # @param [String] path
20
+ # @param [Integer] start
21
+ # @param [Integer] stop
22
+ # @return [Hash]
23
+ def call(action:, arguments: nil)
24
+ command = spawn(action:, arguments:)
25
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
26
+ end
27
+
28
+ private
29
+
30
+ def spawn(action:, arguments:)
31
+ Command
32
+ .new("git")
33
+ .argv(action)
34
+ .argv(*[*arguments])
35
+ .spawn
36
+ end
37
+
38
+ LLM.require "test-cmd.rb"
39
+ Command = Test::Cmd
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::Mkdir LLM::Tool::Mkdir} class implements
6
+ # a tool that can create a tree of new directories.
7
+ class Mkdir < self
8
+ name "mkdir"
9
+ description "create a new directory"
10
+ parameter :path, String, "the path to the directory"
11
+
12
+ ##
13
+ # @param [String] path
14
+ # @return [Hash]
15
+ def call(path:)
16
+ command = spawn(path:)
17
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
18
+ end
19
+
20
+ private
21
+
22
+ def spawn(path:)
23
+ Command
24
+ .new("mkdir")
25
+ .argv("-p", path)
26
+ .spawn
27
+ end
28
+
29
+ LLM.require "test-cmd.rb"
30
+ Command = Test::Cmd
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Pwd LLM::Tool::Pwd} class implements
8
+ # a tool that can reveal the current working directory.
9
+ class Pwd < self
10
+ name "pwd"
11
+ description "returns the current working directory"
12
+
13
+ ##
14
+ # @param [String] path
15
+ # @return [Hash]
16
+ def call
17
+ {ok: true, cwd: Dir.getwd}
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::ReadFile LLM::Tool::ReadFile} class implements
8
+ # a tool that can read the contents of a file. The tool accepts
9
+ # two optional offsets: a start line, and a stop line. Without
10
+ # either the entire file contents are read into memory.
11
+ class ReadFile < self
12
+ name "read-file"
13
+ description "read the contents of a file"
14
+ parameter :path, String, "the path to the file"
15
+ parameter :start, Integer, "start line number"
16
+ parameter :stop, Integer, "stop line number"
17
+ required %i[path]
18
+
19
+ ##
20
+ # @param [String] path
21
+ # @param [Integer] start
22
+ # @param [Integer] stop
23
+ # @return [Hash]
24
+ def call(path:, start: 1, stop: -1)
25
+ content, cursor = nil, 1
26
+ File.open(path, "r") do |f|
27
+ while cursor < start
28
+ f.gets
29
+ cursor += 1
30
+ end
31
+ if stop == -1
32
+ content = f.read
33
+ else
34
+ content = start.upto(stop).map { f.gets }.join
35
+ end
36
+ end
37
+ {ok: true, content:}
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Rg LLM::Tool::Rg} class implements
8
+ # a frontend to the popular 'rg' tool. The tool can
9
+ # recursively search the current working directory
10
+ # for one or more patterns.
11
+ class Rg < self
12
+ name "rg"
13
+ description "recursively search the current directory for lines matching a pattern"
14
+ parameter :patterns, Array[String], "one or more search patterns"
15
+ parameter :path, String, "the path where the search is performed (default is cwd)"
16
+ required %i[patterns]
17
+
18
+ ##
19
+ # @param [String] pattern
20
+ # @return [Hash]
21
+ def call(patterns:, path: Dir.getwd)
22
+ validate!(patterns:, path:)
23
+ command = spawn(patterns:, path:)
24
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
25
+ end
26
+
27
+ private
28
+
29
+ def validate!(patterns:, path:)
30
+ if path == "/"
31
+ raise RuntimeError, "you can't search from the root of the filesystem"
32
+ elsif patterns == ["."]
33
+ raise RuntimeError, "narrow your search"
34
+ end
35
+ end
36
+
37
+ def spawn(patterns:, path:)
38
+ Command.new("rg")
39
+ .argv(*[*patterns].flat_map { ["-e", _1] }, path)
40
+ .spawn
41
+ end
42
+
43
+ LLM.require "test-cmd.rb"
44
+ Command = Test::Cmd
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Shell} class implements a tool that can
8
+ # spawn a command. That can be dangerous given a low-quality
9
+ # model, or a high-quality model that simply makes a bad
10
+ # decision. The risk can be reduced through a confirmation
11
+ # step such as {LLM::Agent.confirm LLM::Agent.confirm}, or
12
+ # by managing the tool loop manually through
13
+ # {LLM::Context LLM::Context}.
14
+ class Shell < self
15
+ name "shell"
16
+ description "run a shell command"
17
+ parameter :name, String, "the command name"
18
+ parameter :arguments, Array[String], "one or more command arguments"
19
+ required %i[name]
20
+
21
+ ##
22
+ # @param [String] name
23
+ # The name of a command
24
+ # @param [Array<String>] arguments
25
+ # One or more command-line arguments
26
+ # @return [Hash]
27
+ def call(name:, arguments: nil)
28
+ command = spawn(name:, arguments:)
29
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
30
+ end
31
+
32
+ private
33
+
34
+ ##
35
+ # @param [String] name
36
+ # @param [Array<String>] arguments
37
+ # @return [Command]
38
+ def spawn(name:, arguments:)
39
+ Command
40
+ .new(name)
41
+ .argv(*[*arguments])
42
+ .spawn
43
+ end
44
+
45
+ LLM.require "test-cmd.rb"
46
+ Command = Test::Cmd
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::SwapText LLM::Tool::SwapText} class
6
+ # implements a tool that can substitute one piece of
7
+ # text for another piece of text in a given file.
8
+ class SwapText < self
9
+ name "swap-text"
10
+ description "Replace an exact snippet in a file"
11
+ parameter :path, String, "Path to file"
12
+ parameter :before, String, "Exact text to replace"
13
+ parameter :after, String, "Replacement text"
14
+ parameter :expected_count, Integer, "How many matches should be replaced"
15
+ required %i[path before after]
16
+
17
+ def call(path:, before:, after:, expected_count: 1)
18
+ content = File.read(path)
19
+ count = content.scan(before).length
20
+ raise "expected #{expected_count} match(es), found #{count}" unless count == expected_count.to_i
21
+ File.write(path, content.sub(before, after))
22
+ {ok: true, replaced: count}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::WriteFile LLM::Tool::WriteFile} class
6
+ # implements a tool that can write a given string to a
7
+ # given file path.
8
+ class WriteFile < self
9
+ name "write-file"
10
+ description "write to a file"
11
+ parameter :path, String, "The file path"
12
+ parameter :content, String, "The file content"
13
+ required %i[path content]
14
+
15
+ ##
16
+ # @param [String] path
17
+ # @param [String] content
18
+ # @return [Hash]
19
+ def call(path:, content:)
20
+ File.open(path, "w") { _1.write(content) }
21
+ {ok: true}
22
+ end
23
+ end
24
+ end
data/lib/llm/tools.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "tools", "*.rb")].each do
4
+ require(_1)
5
+ end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "12.0.0"
4
+ VERSION = "12.1.0"
5
5
  end
@@ -54,6 +54,8 @@ features that didn't make it into the homepage documentation.
54
54
  - [Tracer](#tracer)
55
55
  - [Provider-wide tracer](#provider-wide-tracer)
56
56
  - [Agent-local tracer](#agent-local-tracer)
57
+ - [REPL](#repl)
58
+ - [LLM::Agent](#llmagent)
57
59
  - [Images](#images)
58
60
  - [Generation](#generation)
59
61
  - [Edits](#edits)
@@ -802,6 +804,36 @@ agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, path: "deepseek
802
804
 
803
805
  [Back to top](#table-of-contents)
804
806
 
807
+ ## REPL
808
+
809
+ During the development and operation of agents it can often
810
+ be helpful to drop into a read-eval-print loop. This gives
811
+ you a simple way to confirm the work was successful, inspect
812
+ anything that went wrong (for example, tool call errors), and
813
+ keep talking to the same agent so you can learn about its
814
+ decision-making process and make improvements for future runs.
815
+ It can also be used to ask the agent to correct any errors
816
+ that might have made.
817
+
818
+ ##### LLM::Agent
819
+
820
+ The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
821
+ method requires the [curses](https://github.com/ruby/curses)
822
+ gem to be installed and available for require. It is an
823
+ optional dependency that only becomes required when you
824
+ call the [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
825
+ method.
826
+
827
+ ```ruby
828
+ require "llm"
829
+
830
+ llm = LLM.deepseek(key: ENV["KEY"])
831
+ agent = LLM::Agent.new(llm)
832
+ agent.repl
833
+ ```
834
+
835
+ [Back to top](#table-of-contents)
836
+
805
837
  ## Images
806
838
 
807
839
  The OpenAI, Google, xAI, DeepInfra, and DeepSeek providers have
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.0.0
4
+ version: 12.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - bsdrobert
@@ -459,6 +459,12 @@ files:
459
459
  - lib/llm/providers/xai/images.rb
460
460
  - lib/llm/providers/zai.rb
461
461
  - lib/llm/registry.rb
462
+ - lib/llm/repl.rb
463
+ - lib/llm/repl/input.rb
464
+ - lib/llm/repl/status.rb
465
+ - lib/llm/repl/stream.rb
466
+ - lib/llm/repl/transcript.rb
467
+ - lib/llm/repl/window.rb
462
468
  - lib/llm/response.rb
463
469
  - lib/llm/schema.rb
464
470
  - lib/llm/schema/all_of.rb
@@ -485,6 +491,16 @@ files:
485
491
  - lib/llm/stream/queue.rb
486
492
  - lib/llm/tool.rb
487
493
  - lib/llm/tool/param.rb
494
+ - lib/llm/tools.rb
495
+ - lib/llm/tools/chdir.rb
496
+ - lib/llm/tools/git.rb
497
+ - lib/llm/tools/mkdir.rb
498
+ - lib/llm/tools/pwd.rb
499
+ - lib/llm/tools/read_file.rb
500
+ - lib/llm/tools/rg.rb
501
+ - lib/llm/tools/shell.rb
502
+ - lib/llm/tools/swap_text.rb
503
+ - lib/llm/tools/write_file.rb
488
504
  - lib/llm/tracer.rb
489
505
  - lib/llm/tracer/logger.rb
490
506
  - lib/llm/tracer/null.rb