llm.rb 12.0.0 → 12.2.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +193 -1
  3. data/LICENSE +5 -4
  4. data/README.md +56 -18
  5. data/data/anthropic.json +85 -372
  6. data/data/bedrock.json +498 -3
  7. data/data/deepinfra.json +567 -47
  8. data/data/deepseek.json +4 -0
  9. data/data/google.json +54 -0
  10. data/data/mistral.json +968 -0
  11. data/data/openai.json +51 -0
  12. data/data/xai.json +67 -0
  13. data/data/zai.json +14 -0
  14. data/lib/llm/agent.rb +41 -1
  15. data/lib/llm/context.rb +9 -1
  16. data/lib/llm/function.rb +6 -0
  17. data/lib/llm/json_adapter.rb +29 -3
  18. data/lib/llm/provider.rb +3 -3
  19. data/lib/llm/providers/anthropic.rb +2 -2
  20. data/lib/llm/providers/google.rb +3 -1
  21. data/lib/llm/providers/llamacpp.rb +5 -5
  22. data/lib/llm/providers/mistral/request_adapter/completion.rb +122 -0
  23. data/lib/llm/providers/mistral/request_adapter.rb +20 -0
  24. data/lib/llm/providers/mistral.rb +96 -0
  25. data/lib/llm/repl/input.rb +64 -0
  26. data/lib/llm/repl/status.rb +30 -0
  27. data/lib/llm/repl/stream.rb +46 -0
  28. data/lib/llm/repl/transcript.rb +61 -0
  29. data/lib/llm/repl/window.rb +107 -0
  30. data/lib/llm/repl.rb +78 -0
  31. data/lib/llm/tools/chdir.rb +23 -0
  32. data/lib/llm/tools/git.rb +41 -0
  33. data/lib/llm/tools/mkdir.rb +32 -0
  34. data/lib/llm/tools/pwd.rb +20 -0
  35. data/lib/llm/tools/read_file.rb +40 -0
  36. data/lib/llm/tools/rg.rb +46 -0
  37. data/lib/llm/tools/shell.rb +48 -0
  38. data/lib/llm/tools/swap_text.rb +25 -0
  39. data/lib/llm/tools/write_file.rb +24 -0
  40. data/lib/llm/tools.rb +5 -0
  41. data/lib/llm/version.rb +1 -1
  42. data/lib/llm.rb +9 -0
  43. data/llm.gemspec +1 -1
  44. data/resources/deepdive.md +32 -0
  45. metadata +22 -2
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Window LLM::Repl::Window} class draws the
6
+ # curses screen for the REPL.
7
+ # @api private
8
+ class Window
9
+ ##
10
+ # @return [LLM::Repl::Status]
11
+ attr_reader :status
12
+
13
+ ##
14
+ # @return [LLM::Repl::Transcript]
15
+ attr_reader :transcript
16
+
17
+ ##
18
+ # @return [LLM::Repl::Input]
19
+ attr_reader :input
20
+
21
+ ##
22
+ # @param [LLM::Repl::Status] status
23
+ # @param [LLM::Repl::Transcript] transcript
24
+ # @param [LLM::Repl::Input] input
25
+ # @return [LLM::Repl::Window]
26
+ def initialize(status, transcript, input)
27
+ @status = status
28
+ @transcript = transcript
29
+ @input = input
30
+ end
31
+
32
+ ##
33
+ # @yield
34
+ # @return [void]
35
+ def open
36
+ Curses.init_screen
37
+ Curses.cbreak
38
+ Curses.noecho
39
+ Curses.stdscr.keypad(true)
40
+ yield
41
+ ensure
42
+ Curses.close_screen
43
+ end
44
+
45
+ ##
46
+ # @return [void]
47
+ def redraw
48
+ Curses.clear
49
+ draw_status
50
+ draw_transcript
51
+ draw_input
52
+ Curses.refresh
53
+ end
54
+
55
+ ##
56
+ # @return [Integer]
57
+ def rows
58
+ [Curses.lines - 3, 1].max
59
+ end
60
+
61
+ ##
62
+ # @return [Object]
63
+ def getch
64
+ Curses.getch
65
+ end
66
+
67
+ ##
68
+ # @return [void]
69
+ def scroll_up
70
+ transcript.scroll_up(rows)
71
+ end
72
+
73
+ ##
74
+ # @return [void]
75
+ def scroll_down
76
+ transcript.scroll_down
77
+ end
78
+
79
+ private
80
+
81
+ def draw_status
82
+ Curses.setpos(0, 0)
83
+ Curses.addstr(status.to_s)
84
+ provider = status.provider.to_s
85
+ Curses.setpos(0, [columns - provider.length, 0].max)
86
+ Curses.addstr(provider)
87
+ end
88
+
89
+ def draw_transcript
90
+ visible = transcript.visible(rows)
91
+ visible.each_with_index do |row, index|
92
+ Curses.setpos(index + 2, 0)
93
+ Curses.addstr(row)
94
+ end
95
+ end
96
+
97
+ def draw_input
98
+ Curses.setpos(Curses.lines - 1, 0)
99
+ Curses.clrtoeol
100
+ Curses.addstr(input.to_s)
101
+ end
102
+
103
+ def columns
104
+ Curses.cols
105
+ end
106
+ end
107
+ end
data/lib/llm/repl.rb ADDED
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ LLM.require "curses"
4
+
5
+ module LLM
6
+ ##
7
+ # The {LLM::Repl LLM::Repl} class provides a small
8
+ # read-eval-print loop around an instance of
9
+ # {LLM::Agent LLM::Agent}.
10
+ #
11
+ # It can be used to keep talking to an agent after it
12
+ # has been set up or has performed a task. This can be
13
+ # useful when you want to confirm the agent handled the
14
+ # task correctly, or for it to correct course after a
15
+ # mistake was made.
16
+ class Repl
17
+ require_relative "repl/window"
18
+ require_relative "repl/status"
19
+ require_relative "repl/transcript"
20
+ require_relative "repl/input"
21
+ require_relative "repl/stream"
22
+
23
+ ##
24
+ # @param [LLM::Agent] agent
25
+ # @return [LLM::Repl]
26
+ def initialize(agent)
27
+ @agent = agent
28
+ @provider = agent.llm.name
29
+ @status = Status.new(@provider)
30
+ @transcript = Transcript.new
31
+ @input = Input.new(@provider)
32
+ @window = Window.new(@status, @transcript, @input)
33
+ @stream = Stream.new(self)
34
+ end
35
+
36
+ ##
37
+ # @return [void]
38
+ def start
39
+ window.open do
40
+ loop do
41
+ window.redraw
42
+ text = input.readline(window)
43
+ break if text.nil?
44
+ next if text.empty?
45
+ status.text = "thinking"
46
+ write("user: #{text}\n")
47
+ window.redraw
48
+ write("agent: ")
49
+ agent.talk(text, stream:)
50
+ status.text = "idle"
51
+ write("\n\n")
52
+ end
53
+ end
54
+ end
55
+
56
+ ##
57
+ # @param [String] chars
58
+ # @return [void]
59
+ def write(chars)
60
+ transcript.write(chars)
61
+ window.redraw
62
+ end
63
+
64
+ ##
65
+ # @param [String] value
66
+ # @return [void]
67
+ def status=(value)
68
+ status.text = value
69
+ window.redraw
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :agent, :provider, :stream,
75
+ :status, :transcript, :input,
76
+ :window
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Chdir LLM::Tool::Chdir} class implements
8
+ # a tool that can change the current working directory.
9
+ class Chdir < self
10
+ name "chdir"
11
+ description "change the current working directory"
12
+ parameter :path, String, "the new working directory"
13
+ required %i[path]
14
+
15
+ ##
16
+ # @param [String] path
17
+ # @return [Hash]
18
+ def call(path:)
19
+ Dir.chdir(path)
20
+ {ok: true, cwd: path}
21
+ end
22
+ end
23
+ end
@@ -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.2.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -180,6 +180,15 @@ module LLM
180
180
  LLM::XAI.new(**)
181
181
  end
182
182
 
183
+ ##
184
+ # @param key (see LLM::Mistral#initialize)
185
+ # @param host (see LLM::Mistral#initialize)
186
+ # @return (see LLM::Mistral#initialize)
187
+ def mistral(**)
188
+ lock(:require) { require_relative "llm/providers/mistral" unless defined?(LLM::Mistral) }
189
+ LLM::Mistral.new(**)
190
+ end
191
+
183
192
  ##
184
193
  # @param key (see LLM::ZAI#initialize)
185
194
  # @param host (see LLM::ZAI#initialize)
data/llm.gemspec CHANGED
@@ -5,7 +5,7 @@ require_relative "lib/llm/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "llm.rb"
7
7
  spec.version = LLM::VERSION
8
- spec.authors = ["bsdrobert", "Antar Azri", "Rodrigo Serrano"]
8
+ spec.authors = ["robert", "Antar Azri", "Rodrigo Serrano"]
9
9
  spec.email = ["robert@r.uby.dev"]
10
10
 
11
11
  spec.summary = "Ruby's capable AI runtime"
@@ -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,10 +1,10 @@
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.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - bsdrobert
7
+ - robert
8
8
  - Antar Azri
9
9
  - Rodrigo Serrano
10
10
  bindir: bin
@@ -293,6 +293,7 @@ files:
293
293
  - data/deepinfra.json
294
294
  - data/deepseek.json
295
295
  - data/google.json
296
+ - data/mistral.json
296
297
  - data/openai.json
297
298
  - data/xai.json
298
299
  - data/zai.json
@@ -419,6 +420,9 @@ files:
419
420
  - lib/llm/providers/google/stream_parser.rb
420
421
  - lib/llm/providers/google/utils.rb
421
422
  - lib/llm/providers/llamacpp.rb
423
+ - lib/llm/providers/mistral.rb
424
+ - lib/llm/providers/mistral/request_adapter.rb
425
+ - lib/llm/providers/mistral/request_adapter/completion.rb
422
426
  - lib/llm/providers/ollama.rb
423
427
  - lib/llm/providers/ollama/error_handler.rb
424
428
  - lib/llm/providers/ollama/models.rb
@@ -459,6 +463,12 @@ files:
459
463
  - lib/llm/providers/xai/images.rb
460
464
  - lib/llm/providers/zai.rb
461
465
  - lib/llm/registry.rb
466
+ - lib/llm/repl.rb
467
+ - lib/llm/repl/input.rb
468
+ - lib/llm/repl/status.rb
469
+ - lib/llm/repl/stream.rb
470
+ - lib/llm/repl/transcript.rb
471
+ - lib/llm/repl/window.rb
462
472
  - lib/llm/response.rb
463
473
  - lib/llm/schema.rb
464
474
  - lib/llm/schema/all_of.rb
@@ -485,6 +495,16 @@ files:
485
495
  - lib/llm/stream/queue.rb
486
496
  - lib/llm/tool.rb
487
497
  - lib/llm/tool/param.rb
498
+ - lib/llm/tools.rb
499
+ - lib/llm/tools/chdir.rb
500
+ - lib/llm/tools/git.rb
501
+ - lib/llm/tools/mkdir.rb
502
+ - lib/llm/tools/pwd.rb
503
+ - lib/llm/tools/read_file.rb
504
+ - lib/llm/tools/rg.rb
505
+ - lib/llm/tools/shell.rb
506
+ - lib/llm/tools/swap_text.rb
507
+ - lib/llm/tools/write_file.rb
488
508
  - lib/llm/tracer.rb
489
509
  - lib/llm/tracer/logger.rb
490
510
  - lib/llm/tracer/null.rb