shared_tools 0.2.3 → 0.3.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +594 -42
  4. data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +20 -3
  5. data/lib/shared_tools/mcp/imcp.rb +28 -0
  6. data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
  7. data/lib/shared_tools/mcp.rb +24 -0
  8. data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
  9. data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
  10. data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
  11. data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
  12. data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
  13. data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
  14. data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
  15. data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
  16. data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
  17. data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
  18. data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
  19. data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
  20. data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
  21. data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
  22. data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
  23. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
  24. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
  25. data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
  26. data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
  27. data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
  28. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
  29. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
  30. data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
  31. data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
  32. data/lib/shared_tools/tools/browser.rb +27 -0
  33. data/lib/shared_tools/tools/browser_tool.rb +255 -0
  34. data/lib/shared_tools/tools/calculator_tool.rb +169 -0
  35. data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
  36. data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
  37. data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
  38. data/lib/shared_tools/tools/computer.rb +21 -0
  39. data/lib/shared_tools/tools/computer_tool.rb +207 -0
  40. data/lib/shared_tools/tools/data_science_kit.rb +707 -0
  41. data/lib/shared_tools/tools/database/base_driver.rb +17 -0
  42. data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
  43. data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
  44. data/lib/shared_tools/tools/database.rb +9 -0
  45. data/lib/shared_tools/tools/database_query_tool.rb +313 -0
  46. data/lib/shared_tools/tools/database_tool.rb +99 -0
  47. data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
  48. data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
  49. data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
  50. data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
  51. data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
  52. data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
  53. data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
  54. data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
  55. data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
  56. data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
  57. data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
  58. data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
  59. data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
  60. data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
  61. data/lib/shared_tools/tools/disk.rb +17 -0
  62. data/lib/shared_tools/tools/disk_tool.rb +132 -0
  63. data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
  64. data/lib/shared_tools/tools/doc.rb +8 -0
  65. data/lib/shared_tools/tools/doc_tool.rb +109 -0
  66. data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
  67. data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
  68. data/lib/shared_tools/tools/docker.rb +8 -0
  69. data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
  70. data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
  71. data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
  72. data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
  73. data/lib/shared_tools/tools/eval.rb +10 -0
  74. data/lib/shared_tools/tools/eval_tool.rb +139 -0
  75. data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
  76. data/lib/shared_tools/tools/version.rb +7 -0
  77. data/lib/shared_tools/tools/weather_tool.rb +197 -0
  78. data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
  79. data/lib/shared_tools/tools.rb +16 -0
  80. data/lib/shared_tools/version.rb +1 -1
  81. data/lib/shared_tools.rb +9 -24
  82. metadata +189 -68
  83. data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
  84. data/lib/shared_tools/llm_rb.rb +0 -9
  85. data/lib/shared_tools/omniai.rb +0 -9
  86. data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
  87. data/lib/shared_tools/raix.rb +0 -9
  88. data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
  89. data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
  90. data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
  91. data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
  92. data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
  93. data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
  94. data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
  95. data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
  96. data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
  97. data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
  98. data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
  99. data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -15
  100. data/lib/shared_tools/ruby_llm/mcp.rb +0 -12
  101. data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
  102. data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
  103. data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
  104. data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
  105. data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
  106. data/lib/shared_tools/ruby_llm.rb +0 -12
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileCreateTool.new(root: "./project")
10
+ # tool.execute(path: "./README.md")
11
+ class FileCreateTool < ::RubyLLM::Tool
12
+ def self.name = 'disk_file_create'
13
+
14
+ description "Creates a file."
15
+
16
+ params do
17
+ string :path, description: "a path to the file (e.g. `./README.md`)"
18
+ end
19
+
20
+ def initialize(driver: nil, logger: nil)
21
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
22
+ @logger = logger || RubyLLM.logger
23
+ end
24
+
25
+ # @param path [String]
26
+ #
27
+ # @return [String]
28
+ def execute(path:)
29
+ @logger.info("#{self.class.name}#execute path=#{path.inspect}")
30
+ @driver.file_create(path:)
31
+ rescue SecurityError => e
32
+ @logger.error(e.message)
33
+ raise e
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileDeleteTool.new(root: "./project")
10
+ # tool.execute(path: "./README.md")
11
+ class FileDeleteTool < ::RubyLLM::Tool
12
+ def self.name = 'disk_file_delete'
13
+
14
+ description "Deletes a file."
15
+
16
+ params do
17
+ string :path, description: "a path to the file (e.g. `./README.md`)"
18
+ end
19
+
20
+ def initialize(driver: nil, logger: nil)
21
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
22
+ @logger = logger || RubyLLM.logger
23
+ end
24
+
25
+ # @param path [String]
26
+ #
27
+ # @raise [SecurityError]
28
+ #
29
+ # @return [String]
30
+ def execute(path:)
31
+ @logger.info("#{self.class.name}#execute path=#{path.inspect}")
32
+ @driver.file_delete(path:)
33
+ rescue SecurityError => e
34
+ @logger.error(e.message)
35
+ raise e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileMoveTool.new(root: "./project")
10
+ # tool.execute(
11
+ # path: "./README.txt",
12
+ # destination: "./README.md",
13
+ # )
14
+ class FileMoveTool < ::RubyLLM::Tool
15
+ def self.name = 'disk_file_move'
16
+
17
+ description "Moves a file."
18
+
19
+ params do
20
+ string :path, description: "a path (e.g. `./old.rb`)"
21
+ string :destination, description: "a path (e.g. `./new.rb`)"
22
+ end
23
+
24
+ def initialize(driver: nil, logger: nil)
25
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
26
+ @logger = logger || RubyLLM.logger
27
+ end
28
+
29
+ # @param path [String]
30
+ # @param destination [String]
31
+ #
32
+ # @return [String]
33
+ def execute(path:, destination:)
34
+ @logger.info("#{self.class.name}#execute path=#{path.inspect} destination=#{destination.inspect}")
35
+ @driver.file_move(path:, destination:)
36
+ rescue SecurityError => e
37
+ @logger.info("ERROR: #{e.message}")
38
+ raise e
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileReadTool.new
10
+ # tool.execute(path: "./README.md") # => "..."
11
+ class FileReadTool < ::RubyLLM::Tool
12
+ def self.name = 'disk_file_read'
13
+
14
+ description "Reads the contents of a file."
15
+
16
+ params do
17
+ string :path, description: "a path (e.g. `./main.rb`)"
18
+ end
19
+
20
+ # @param driver [SharedTools::Tools::Disk::BaseDriver] optional, defaults to LocalDriver with current directory
21
+ # @param logger [Logger] optional logger
22
+ def initialize(driver: nil, logger: nil)
23
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
24
+ @logger = logger || RubyLLM.logger
25
+ end
26
+
27
+ # @param path [String]
28
+ #
29
+ # @return [String]
30
+ def execute(path:)
31
+ @logger.info("#{self.class.name}#execute path=#{path}")
32
+ @driver.file_read(path:)
33
+ rescue StandardError => e
34
+ @logger.error(e.message)
35
+ raise e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileReadTool.new(root: "./project")
10
+ # tool.execute(
11
+ # old_text: 'puts "ABC"',
12
+ # new_text: 'puts "DEF"',
13
+ # path: "README.md"
14
+ # )
15
+ class FileReplaceTool < ::RubyLLM::Tool
16
+ def self.name = 'disk_file_replace'
17
+
18
+ description "Replaces a specific string in a file (old_text => new_text)."
19
+
20
+ params do
21
+ string :old_text, description: "the old text (e.g. `puts 'ABC'`)"
22
+ string :new_text, description: "the new text (e.g. `puts 'DEF'`)"
23
+ string :path, description: "a path (e.g. `./main.rb`)"
24
+ end
25
+
26
+ def initialize(driver: nil, logger: nil)
27
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
28
+ @logger = logger || RubyLLM.logger
29
+ end
30
+
31
+ # @param path [String]
32
+ # @param old_text [String]
33
+ # @param new_text [String]
34
+ def execute(old_text:, new_text:, path:)
35
+ @logger.info %(#{self.class.name}#execute old_text="#{old_text}" new_text="#{new_text}" path="#{path}")
36
+ @driver.file_replace(old_text:, new_text:, path:)
37
+ rescue SecurityError => e
38
+ @logger.error(e.message)
39
+ raise e
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "local_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # @example
9
+ # tool = SharedTools::Tools::Disk::FileWriteTool.new(root: "./project")
10
+ # tool.execute(path: "./README.md", text: "Hello World")
11
+ class FileWriteTool < ::RubyLLM::Tool
12
+ def self.name = 'disk_file_write'
13
+
14
+ description "Writes the contents of a file."
15
+
16
+ params do
17
+ string :path, description: "a path for the file (e.g. `./main.rb`)"
18
+ string :text, description: "the text to write to the file (e.g. `puts 'Hello World'`)"
19
+ end
20
+
21
+ def initialize(driver: nil, logger: nil)
22
+ @driver = driver || SharedTools::Tools::Disk::LocalDriver.new(root: Dir.pwd)
23
+ @logger = logger || RubyLLM.logger
24
+ end
25
+
26
+ # @param path [String]
27
+ # @param text [String]
28
+ #
29
+ # @return [String]
30
+ def execute(path:, text:)
31
+ @logger.info("#{self.class.name}#execute path=#{path}")
32
+ @driver.file_write(path:, text:)
33
+ rescue SecurityError => e
34
+ @logger.error("ERROR: #{e.message}")
35
+ raise e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_driver"
4
+
5
+ module SharedTools
6
+ module Tools
7
+ module Disk
8
+ # A driver for interacting with a disk via various operations
9
+ class LocalDriver < BaseDriver
10
+ # @raise [SecurityError]
11
+ #
12
+ # @param path [String]
13
+ def directory_create(path:)
14
+ FileUtils.mkdir_p(resolve!(path:))
15
+ end
16
+
17
+ # @param path [String]
18
+ def directory_delete(path:)
19
+ FileUtils.rmdir(resolve!(path:))
20
+ end
21
+
22
+ # @param path [String] optional
23
+ def directory_list(path: ".")
24
+ Dir.chdir(resolve!(path:)) do
25
+ Dir.glob("**/*").map { |path| summarize(path:) }.join("\n")
26
+ end
27
+ end
28
+
29
+ # @param path [String]
30
+ # @param destination [String]
31
+ def directory_move(path:, destination:)
32
+ FileUtils.mv(resolve!(path:), resolve!(path: destination))
33
+ end
34
+
35
+ # @param path [String]
36
+ def file_create(path:)
37
+ path = resolve!(path:)
38
+ FileUtils.touch(path) unless File.exist?(path)
39
+ end
40
+
41
+ # @param path [String]
42
+ def file_delete(path:)
43
+ File.delete(resolve!(path:))
44
+ end
45
+
46
+ # @param path [String]
47
+ # @param destination [String]
48
+ def file_move(path:, destination:)
49
+ FileUtils.mv(resolve!(path:), resolve!(path: destination))
50
+ end
51
+
52
+ # @param path [String]
53
+ #
54
+ # @return [String]
55
+ def file_read(path:)
56
+ File.read(resolve!(path:))
57
+ end
58
+
59
+ # @param old_text [String]
60
+ # @param new_text [String]
61
+ # @param path [String]
62
+ def file_replace(old_text:, new_text:, path:)
63
+ path = resolve!(path:)
64
+ contents = File.read(path)
65
+ text = contents.gsub(old_text, new_text)
66
+ File.write(path, text)
67
+ end
68
+
69
+ # @param path [String]
70
+ # @param text [String]
71
+ def file_write(path:, text:)
72
+ File.write(resolve!(path:), text)
73
+ end
74
+
75
+ protected
76
+
77
+ # @param path [String]
78
+ #
79
+ # @raise [SecurityError]
80
+ #
81
+ # @return Pathname
82
+ def resolve!(path:)
83
+ @root.join(path).tap do |resolved|
84
+ relative = resolved.ascend.any? { |ancestor| ancestor.eql?(@root) }
85
+ raise SecurityError, "unknown path=#{resolved}" unless relative
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection loader for all disk tools
4
+ # Usage: require 'shared_tools/tools/disk'
5
+
6
+ require 'shared_tools'
7
+
8
+ require_relative 'disk/file_read_tool'
9
+ require_relative 'disk/file_write_tool'
10
+ require_relative 'disk/file_create_tool'
11
+ require_relative 'disk/file_delete_tool'
12
+ require_relative 'disk/file_move_tool'
13
+ require_relative 'disk/file_replace_tool'
14
+ require_relative 'disk/directory_list_tool'
15
+ require_relative 'disk/directory_create_tool'
16
+ require_relative 'disk/directory_delete_tool'
17
+ require_relative 'disk/directory_move_tool'
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # A tool for interacting with files and directories. Be careful using as it can perform actions on your computer!
8
+ #
9
+ # @example
10
+ # disk = SharedTools::Tools::DiskTool.new
11
+ # disk.execute(action: SharedTools::Tools::DiskTool::Action::FILE_CREATE, path: "./demo.rb")
12
+ # disk.execute(action: SharedTools::Tools::DiskTool::Action::FILE_WRITE, path: "./demo.rb", text: "puts 'Hello'")
13
+ # disk.execute(action: SharedTools::Tools::DiskTool::Action::FILE_READ, path: "./demo.rb")
14
+ # disk.execute(action: SharedTools::Tools::DiskTool::Action::FILE_DELETE, path: "./demo.rb")
15
+ class DiskTool < ::RubyLLM::Tool
16
+ def self.name = 'disk_tool'
17
+ description <<~TEXT
18
+ A tool for interacting with a system. It is able to list, create, delete, move and modify directories and files.
19
+ TEXT
20
+
21
+ module Action
22
+ DIRECTORY_CREATE = "directory_create"
23
+ DIRECTORY_DELETE = "directory_delete"
24
+ DIRECTORY_MOVE = "directory_move"
25
+ DIRECTORY_LIST = "directory_list"
26
+ FILE_CREATE = "file_create"
27
+ FILE_DELETE = "file_delete"
28
+ FILE_MOVE = "file_move"
29
+ FILE_READ = "file_read"
30
+ FILE_WRITE = "file_write"
31
+ FILE_REPLACE = "file_replace"
32
+ end
33
+
34
+ ACTIONS = [
35
+ Action::DIRECTORY_CREATE,
36
+ Action::DIRECTORY_DELETE,
37
+ Action::DIRECTORY_MOVE,
38
+ Action::DIRECTORY_LIST,
39
+ Action::FILE_CREATE,
40
+ Action::FILE_DELETE,
41
+ Action::FILE_MOVE,
42
+ Action::FILE_READ,
43
+ Action::FILE_WRITE,
44
+ Action::FILE_REPLACE,
45
+ ].freeze
46
+
47
+ params do
48
+ string :action, description: <<~TEXT.strip
49
+ Options:
50
+ * `#{Action::DIRECTORY_CREATE}`: creates a directory at a specific `path`
51
+ * `#{Action::DIRECTORY_DELETE}`: deletes a directory at a specific `path`
52
+ * `#{Action::DIRECTORY_MOVE}`: moves a directory from `path` to (`to`)
53
+ * `#{Action::DIRECTORY_LIST}`: lists the contents of a directory at a specific `path` (use '.' for root)
54
+ * `#{Action::FILE_CREATE}`: creates a file at a specific `path`
55
+ * `#{Action::FILE_DELETE}`: deletes a file at a specific `path`
56
+ * `#{Action::FILE_MOVE}`: moves a file from `path` to another
57
+ * `#{Action::FILE_READ}`: reads the contents of a file at a specific path
58
+ * `#{Action::FILE_WRITE}`: writes the contents of a file at a specific path
59
+ * `#{Action::FILE_REPLACE}`: replaces the contents of a file at a specific path
60
+ TEXT
61
+
62
+ string :path, description: <<~TEXT.strip
63
+ A file or directory path that is required for the following actions:
64
+ * `#{Action::DIRECTORY_CREATE}`
65
+ * `#{Action::DIRECTORY_DELETE}`
66
+ * `#{Action::DIRECTORY_MOVE}`
67
+ * `#{Action::DIRECTORY_LIST}`
68
+ * `#{Action::FILE_DELETE}`
69
+ * `#{Action::FILE_READ}`
70
+ * `#{Action::FILE_WRITE}`
71
+ * `#{Action::FILE_REPLACE}`
72
+ TEXT
73
+
74
+ string :destination, description: <<~TEXT.strip, required: false
75
+ A file or directory path that is required for the following actions:
76
+ * `#{Action::DIRECTORY_MOVE}`
77
+ * `#{Action::FILE_MOVE}`
78
+ TEXT
79
+
80
+ string :text, description: <<~TEXT.strip, required: false
81
+ The text to be written to a file for the `#{Action::FILE_WRITE}` action.
82
+ TEXT
83
+
84
+ string :old_text, description: <<~TEXT.strip, required: false
85
+ The old text to be replaced in a file for the `#{Action::FILE_REPLACE}` action.
86
+ TEXT
87
+
88
+ string :new_text, description: <<~TEXT.strip, required: false
89
+ The new text to replace in a few file for the `#{Action::FILE_REPLACE}` action.
90
+ TEXT
91
+ end
92
+
93
+
94
+ # @param driver [SharedTools::Tools::Disk::BaseDriver] optional, defaults to LocalDriver with current directory
95
+ # @param logger [Logger] optional logger
96
+ def initialize(driver: nil, logger: nil)
97
+ @driver = driver || Disk::LocalDriver.new(root: Dir.pwd)
98
+ @logger = logger || RubyLLM.logger
99
+ end
100
+
101
+ # @param action [String]
102
+ # @param path [String]
103
+ # @param destination [String] optional
104
+ # @param old_text [String] optional
105
+ # @param new_text [String] optional
106
+ # @param text [String] optional
107
+ def execute(action:, path:, destination: nil, old_text: nil, new_text: nil, text: nil)
108
+ @logger.info({
109
+ action:,
110
+ path:,
111
+ destination:,
112
+ old_text:,
113
+ new_text:,
114
+ text:,
115
+ }.compact.map { |key, value| "#{key}=#{value.inspect}" }.join(" "))
116
+
117
+ case action
118
+ when Action::DIRECTORY_CREATE then @driver.directory_create(path:)
119
+ when Action::DIRECTORY_DELETE then @driver.directory_delete(path:)
120
+ when Action::DIRECTORY_MOVE then @driver.directory_move(path:, destination:)
121
+ when Action::DIRECTORY_LIST then @driver.directory_list(path:)
122
+ when Action::FILE_CREATE then @driver.file_create(path:)
123
+ when Action::FILE_DELETE then @driver.file_delete(path:)
124
+ when Action::FILE_MOVE then @driver.file_move(path:, destination:)
125
+ when Action::FILE_READ then @driver.file_read(path:)
126
+ when Action::FILE_WRITE then @driver.file_write(path:, text:)
127
+ when Action::FILE_REPLACE then @driver.file_replace(old_text:, new_text:, path:)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ # Credit: https://max.engineer/giant-pdf-llm
3
+
4
+ begin
5
+ require "pdf-reader"
6
+ rescue LoadError
7
+ # pdf-reader is optional - will raise error when tool is used without it
8
+ end
9
+
10
+ module SharedTools
11
+ module Tools
12
+ module Doc
13
+ # @example
14
+ # tool = SharedTools::Tools::Doc::PdfReaderTool.new
15
+ # tool.execute(doc_path: "./document.pdf", page_numbers: "1, 5, 10")
16
+ class PdfReaderTool < ::RubyLLM::Tool
17
+ def self.name = 'doc_pdf_read'
18
+
19
+ description "Read the text of any set of pages from a PDF document."
20
+
21
+ params do
22
+ string :page_numbers, description: 'Comma-separated page numbers (first page: 1). (e.g. "12, 14, 15")'
23
+ string :doc_path, description: "Path to the PDF document."
24
+ end
25
+
26
+ # @param logger [Logger] optional logger
27
+ def initialize(logger: nil)
28
+ @logger = logger || RubyLLM.logger
29
+ end
30
+
31
+ # @param page_numbers [String] comma-separated page numbers
32
+ # @param doc_path [String] path to PDF file
33
+ #
34
+ # @return [Hash] extraction result
35
+ def execute(page_numbers:, doc_path:)
36
+ raise LoadError, "PdfReaderTool requires the 'pdf-reader' gem. Install it with: gem install pdf-reader" unless defined?(PDF::Reader)
37
+
38
+ @logger.info("Reading PDF: #{doc_path}, pages: #{page_numbers}")
39
+
40
+ begin
41
+ @doc ||= PDF::Reader.new(doc_path)
42
+ @logger.debug("PDF loaded successfully, total pages: #{@doc.pages.size}")
43
+
44
+ page_numbers = page_numbers.split(",").map { |num| num.strip.to_i }
45
+ @logger.debug("Processing pages: #{page_numbers.join(", ")}")
46
+
47
+ # Validate page numbers
48
+ total_pages = @doc.pages.size
49
+ invalid_pages = page_numbers.select { |num| num < 1 || num > total_pages }
50
+
51
+ if invalid_pages.any?
52
+ @logger.warn("Invalid page numbers requested: #{invalid_pages.join(", ")}. Document has #{total_pages} pages.")
53
+ end
54
+
55
+ # Filter valid pages and map to content
56
+ valid_pages = page_numbers.select { |num| num >= 1 && num <= total_pages }
57
+ pages = valid_pages.map { |num| [num, @doc.pages[num.to_i - 1]] }
58
+
59
+ result = {
60
+ total_pages: total_pages,
61
+ requested_pages: page_numbers,
62
+ invalid_pages: invalid_pages,
63
+ pages: pages.map { |num, p|
64
+ @logger.debug("Extracted text from page #{num} (#{p&.text&.bytesize || 0} bytes)")
65
+ { page: num, text: p&.text }
66
+ },
67
+ }
68
+
69
+ @logger.info("Successfully extracted #{pages.size} pages from PDF")
70
+ result
71
+ rescue => e
72
+ @logger.error("Failed to read PDF '#{doc_path}': #{e.message}")
73
+ { error: e.message }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection loader for all doc tools
4
+ # Usage: require 'shared_tools/tools/doc'
5
+
6
+ require 'shared_tools'
7
+
8
+ require_relative 'doc/pdf_reader_tool'
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # A tool for reading and processing documents
8
+ class DocTool < ::RubyLLM::Tool
9
+ def self.name = 'doc_tool'
10
+
11
+ module Action
12
+ PDF_READ = "pdf_read"
13
+ end
14
+
15
+ ACTIONS = [
16
+ Action::PDF_READ,
17
+ ].freeze
18
+
19
+ description <<~TEXT
20
+ Read and process various document formats.
21
+
22
+ ## Actions:
23
+
24
+ 1. `#{Action::PDF_READ}` - Read specific pages from a PDF document
25
+ Required: "action": "pdf_read", "doc_path": "[path to PDF]", "page_numbers": "[comma-separated page numbers]"
26
+
27
+ The page_numbers parameter accepts:
28
+ - Single page: "5"
29
+ - Multiple pages: "1, 3, 5"
30
+ - Range notation: "1-10" or "1, 3-5, 10"
31
+
32
+ ## Examples:
33
+
34
+ Read single page from PDF
35
+ {"action": "#{Action::PDF_READ}", "doc_path": "./document.pdf", "page_numbers": "1"}
36
+
37
+ Read multiple pages
38
+ {"action": "#{Action::PDF_READ}", "doc_path": "./report.pdf", "page_numbers": "1, 5, 10"}
39
+
40
+ Read page range
41
+ {"action": "#{Action::PDF_READ}", "doc_path": "./book.pdf", "page_numbers": "10-15"}
42
+
43
+ Read specific pages with range
44
+ {"action": "#{Action::PDF_READ}", "doc_path": "./manual.pdf", "page_numbers": "1, 5-8, 15, 20-25"}
45
+ TEXT
46
+
47
+ params do
48
+ string :action, description: <<~TEXT.strip
49
+ The document action to perform. Options:
50
+ * `#{Action::PDF_READ}`: Read pages from a PDF document
51
+ TEXT
52
+
53
+ string :doc_path, description: <<~TEXT.strip, required: false
54
+ Path to the document file. Required for the following actions:
55
+ * `#{Action::PDF_READ}`
56
+ TEXT
57
+
58
+ string :page_numbers, description: <<~TEXT.strip, required: false
59
+ Comma-separated page numbers to read (first page is 1).
60
+ Examples: "1", "1, 3, 5", "1-10", "1, 5-8, 15"
61
+ Required for the following actions:
62
+ * `#{Action::PDF_READ}`
63
+ TEXT
64
+ end
65
+
66
+ # @param logger [Logger] optional logger
67
+ def initialize(logger: nil)
68
+ @logger = logger || RubyLLM.logger
69
+ end
70
+
71
+ # @param action [String] the action to perform
72
+ # @param doc_path [String, nil] path to document
73
+ # @param page_numbers [String, nil] page numbers to read
74
+ #
75
+ # @return [Hash] execution result
76
+ def execute(action:, doc_path: nil, page_numbers: nil)
77
+ @logger.info("DocTool#execute action=#{action}")
78
+
79
+ case action.to_s.downcase
80
+ when Action::PDF_READ
81
+ require_param!(:doc_path, doc_path)
82
+ require_param!(:page_numbers, page_numbers)
83
+ pdf_reader_tool.execute(doc_path: doc_path, page_numbers: page_numbers)
84
+ else
85
+ { error: "Unsupported action: #{action}. Supported actions are: #{ACTIONS.join(', ')}" }
86
+ end
87
+ rescue StandardError => e
88
+ @logger.error("DocTool execution failed: #{e.message}")
89
+ { error: e.message }
90
+ end
91
+
92
+ private
93
+
94
+ # @param name [Symbol]
95
+ # @param value [Object]
96
+ #
97
+ # @raise [ArgumentError]
98
+ # @return [void]
99
+ def require_param!(name, value)
100
+ raise ArgumentError, "#{name} param is required for this action" if value.nil?
101
+ end
102
+
103
+ # @return [Doc::PdfReaderTool]
104
+ def pdf_reader_tool
105
+ @pdf_reader_tool ||= Doc::PdfReaderTool.new(logger: @logger)
106
+ end
107
+ end
108
+ end
109
+ end