shared_tools 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ae3eef4f29e12a8be7ff8db5eec7e156bcfa1e767d5375b2ffde66db35629ef
4
- data.tar.gz: 6f73bca50589be410d134eb2c02be5451bed2f875dea5854984181ff9f37fc44
3
+ metadata.gz: bef20237112478305bf9cdb8e787ac9f71b9e3c32c3349e27f5e5bfead02e182
4
+ data.tar.gz: b14559042c705dede19698e6cd0a77eaaa95e7e8c098027e7965688f5135588f
5
5
  SHA512:
6
- metadata.gz: 42a193b5df012a25909c94a28d60e1d66347683d32bc29979a578e4fbf705738c02053625c45828c7029fe2473fa8cf40ca2e83a67ed29599ac10ab4436f2d4c
7
- data.tar.gz: 57bb4e3b32137687b81257adf786d684fc1bb521a8d661fb23ea057047c64ae78e1f2af0f2273199093d1194d8553276bbba3302f76d3942677b7c24a78dfcd7
6
+ metadata.gz: 12d20678d2c41ad9c5c2a27bf6a6fbe57cee1e70f68554c0f13fcf20ceb5b5fadee5762a9f4099db282cdd005df2d657b4e8dce16cbfcc5a39e51847731d7cda
7
+ data.tar.gz: 7c4e14b09e87cf4ff7a2e6f7f679e19c6a62dc3be368c842959a683eafa6ea7d8ce73cc3f02311bb645a53a72c40cb8b7f3a9a8507f11dec3fe3fd259f90cc17
data/CHANGELOG.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## [0.1.0] - 2025-06-05
3
+ ## Unreleased
4
4
 
5
- ### Added
5
+ ## Released
6
6
 
7
+ ### [0.1.2] 2025-06-10
8
+ - added `zeitwerk` gem
9
+
10
+ ### [0.1.0] - 2025-06-05
7
11
  - Initial gem release
8
12
  - SharedTools core module with automatic logger integration
9
13
  - RubyLlm tools: EditFile, ListFiles, PdfPageReader, ReadFile, RunShellCommand
data/README.md CHANGED
@@ -1,9 +1,14 @@
1
- # shared_tools
2
- A Ruby gem providing a collection of shared tools and utilities for Ruby applications, including configurable logging and AI-related tools.
1
+ <div align="center">
2
+ <h1>Shared Tools</h1>
3
+ <img src="images/shared_tools.png" alt="Two Robots sharing the same set of tools.">
4
+ </div>
3
5
 
4
- ## Libraries Supported
6
+ A Ruby gem providing a collection of common tools (call-back functions) for use with the following gems:
5
7
 
6
- - ruby_llm: multi-provider
8
+ - ruby_llm: multi-provider `gem install ruby_llm`
9
+ - llm: multi-provider `gem install llm.rb`
10
+ - omniai: multi-provider `gem install omniai-tools` (Not part of the SharedTools namespace)
11
+ - more to come ...
7
12
 
8
13
  ## Installation
9
14
 
@@ -27,86 +32,34 @@ gem install shared_tools
27
32
 
28
33
  ## Usage
29
34
 
30
- ```ruby
31
- # To load all tools for a specific library
32
- require 'shared_tools/ruby_llm'
33
-
34
- # OR you can load a specific tool
35
- require 'shared_tools/ruby_llm/edit_file'
36
- ```
37
-
38
- ## Shared Logging
39
-
40
- All classes within the SharedTools namespace automatically have access to a configurable logger without requiring any explicit includes or setup.
41
-
42
- ### Basic Usage
43
-
44
- Within any class in the SharedTools namespace, you can directly use the logger:
35
+ ### Basic Loading
45
36
 
46
37
  ```ruby
47
- module SharedTools
48
- class MyTool
49
- def perform_action
50
- logger.info "Starting action"
51
- # Do something
52
- logger.debug "Details about action"
53
- # Handle errors
54
- rescue => e
55
- logger.error "Action failed: #{e.message}"
56
- raise
57
- ensure
58
- logger.info "Action completed"
59
- end
60
- end
61
- end
38
+ require 'shared_tools'
62
39
  ```
63
40
 
64
- ### Configuration
41
+ ### Loading RubyLLM Tools
65
42
 
66
- The logger can be configured using the `configure_logger` method:
43
+ RubyLLM tools are loaded conditionally when needed:
67
44
 
68
45
  ```ruby
69
- # Configure the logger in your application initialization
70
- SharedTools.configure_logger do |config|
71
- config.level = Logger::DEBUG # Set log level
72
- config.log_device = "logs/shared_tools.log" # Set output file
73
- config.formatter = proc do |severity, time, progname, msg|
74
- "[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
75
- end
76
- end
77
- ```
78
-
79
- ### Using with Rails
46
+ require 'shared_tools'
80
47
 
81
- To use the SharedTools logger with Rails and make it use the same logger instance:
48
+ # Load all RubyLLM tools (requires ruby_llm gem to be available and loaded first)
49
+ require 'shared_tools/ruby_llm'
82
50
 
83
- ```ruby
84
- # In config/initializers/shared_tools.rb
85
- Rails.application.config.after_initialize do
86
- # Make SharedTools use the Rails logger
87
- SharedTools.logger = Rails.logger
88
-
89
- # Alternatively, configure the Rails logger to use SharedTools settings
90
- # SharedTools.configure_logger do |config|
91
- # config.level = Rails.logger.level
92
- # config.log_device = Rails.logger.instance_variable_get(:@logdev).dev
93
- # end
94
- # Rails.logger = SharedTools.logger
95
-
96
- Rails.logger.info "SharedTools integrated with Rails logger"
97
- end
51
+ # Or load a specific tool directly
52
+ require 'shared_tools/ruby_llm/edit_file'
53
+ require 'shared_tools/ruby_llm/read_file'
54
+ require 'shared_tools/ruby_llm/python_eval'
55
+ # etc.
98
56
  ```
99
57
 
100
- ### Available Log Levels
58
+ ### Rails and Autoloader Compatibility
101
59
 
102
- The logger supports the standard Ruby Logger levels:
60
+ This gem uses Zeitwerk for autoloading, making it fully compatible with Rails and other Ruby applications that use modern autoloaders. RubyLLM tools are excluded from autoloading and loaded manually to avoid namespace conflicts.
103
61
 
104
- - `logger.debug` - Detailed debug information
105
- - `logger.info` - General information messages
106
- - `logger.warn` - Warning messages
107
- - `logger.error` - Error messages
108
- - `logger.fatal` - Fatal error messages
109
62
 
110
- ### Thread Safety
63
+ ### Special Thanks
111
64
 
112
- The shared logger is thread-safe and can be used across multiple threads in your application.
65
+ A special shout-out to Kevin's [omniai-tools](https://github.com/your-github-url/omniai-tools) gem, which is a curated collection of tools for use with his OmniAI gem.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ verify_gem :llm_rb
7
+
8
+ RunShellCommand = LLM.function(:system) do |fn|
9
+ fn.description "Run a shell command"
10
+
11
+ fn.params do |schema|
12
+ schema.object(command: schema.string.required)
13
+ end
14
+
15
+ fn.define do |params|
16
+ ro, wo = IO.pipe
17
+ re, we = IO.pipe
18
+ Process.wait Process.spawn(params.command, out: wo, err: we)
19
+ [wo, we].each(&:close)
20
+ { stderr: re.read, stdout: ro.read }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SharedTools.verify_gem :llm_rb
4
+
5
+ Dir.glob(File.join(__dir__, "llm_rb", "*.rb")).each do |file|
6
+ require file
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ SharedTools.verify_gem :omniai
4
+
5
+ Dir.glob(File.join(__dir__, "omniai", "*.rb")).each do |file|
6
+ require file
7
+ end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("ruby_llm") unless defined?(RubyLLM)
4
- require("shared_tools") unless defined?(SharedTools)
3
+ require_relative '../../shared_tools'
5
4
 
6
5
  module SharedTools
7
- class EditFile < RubyLLM::Tool
6
+ verify_gem :ruby_llm
7
+
8
+ class EditFile < ::RubyLLM::Tool
8
9
 
9
10
  description <<~DESCRIPTION
10
11
  Make edits to a text file.
@@ -22,47 +23,47 @@ module SharedTools
22
23
  param :replace_all, desc: "Whether to replace all occurrences (true) or just the first one (false)", required: false
23
24
 
24
25
  def execute(path:, old_str:, new_str:, replace_all: false)
25
- logger.info("Editing file: #{path}")
26
+ RubyLLM.logger.info("Editing file: #{path}")
26
27
 
27
28
  # Normalize path to absolute path
28
29
  absolute_path = File.absolute_path(path)
29
30
 
30
31
  if File.exist?(absolute_path)
31
- logger.debug("File exists, reading content")
32
+ RubyLLM.logger.debug("File exists, reading content")
32
33
  content = File.read(absolute_path)
33
34
  else
34
- logger.debug("File doesn't exist, creating new file")
35
+ RubyLLM.logger.debug("File doesn't exist, creating new file")
35
36
  content = ""
36
37
  end
37
38
 
38
39
  matches = content.scan(old_str).size
39
- logger.debug("Found #{matches} matches for the string to replace")
40
+ RubyLLM.logger.debug("Found #{matches} matches for the string to replace")
40
41
 
41
42
  if matches == 0
42
- logger.warn("No matches found for the string to replace. File will remain unchanged.")
43
+ RubyLLM.logger.warn("No matches found for the string to replace. File will remain unchanged.")
43
44
  return { success: false, warning: "No matches found for the string to replace" }
44
45
  end
45
46
 
46
47
  if matches > 1 && !replace_all
47
- logger.warn("Multiple matches (#{matches}) found for the string to replace. Only the first occurrence will be replaced.")
48
+ RubyLLM.logger.warn("Multiple matches (#{matches}) found for the string to replace. Only the first occurrence will be replaced.")
48
49
  end
49
50
 
50
51
  if replace_all
51
52
  updated_content = content.gsub(old_str, new_str)
52
53
  replaced_count = matches
53
- logger.info("Replacing all #{matches} occurrences")
54
+ RubyLLM.logger.info("Replacing all #{matches} occurrences")
54
55
  else
55
56
  updated_content = content.sub(old_str, new_str)
56
57
  replaced_count = 1
57
- logger.info("Replacing first occurrence only")
58
+ RubyLLM.logger.info("Replacing first occurrence only")
58
59
  end
59
60
 
60
61
  File.write(absolute_path, updated_content)
61
62
 
62
- logger.info("Successfully updated file: #{path}")
63
+ RubyLLM.logger.info("Successfully updated file: #{path}")
63
64
  { success: true, matches: matches, replaced: replaced_count }
64
65
  rescue => e
65
- logger.error("Failed to edit file '#{path}': #{e.message}")
66
+ RubyLLM.logger.error("Failed to edit file '#{path}': #{e.message}")
66
67
  { error: e.message }
67
68
  end
68
69
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("ruby_llm") unless defined?(RubyLLM)
4
- require("shared_tools") unless defined?(SharedTools)
3
+ require_relative '../../shared_tools'
5
4
 
6
5
  module SharedTools
7
- class ListFiles < RubyLLM::Tool
6
+ verify_gem :ruby_llm
7
+
8
+ class ListFiles < ::RubyLLM::Tool
8
9
 
9
10
  description "List files and directories at a given path. If no path is provided, lists files in the current directory."
10
11
  param :path, desc: "Optional relative path to list files from. Defaults to current directory if not provided."
11
12
 
12
13
  def execute(path: Dir.pwd)
13
- logger.info("Listing files in path: #{path}")
14
+ RubyLLM.logger.info("Listing files in path: #{path}")
14
15
 
15
16
  # Convert to absolute path for consistency
16
17
  absolute_path = File.absolute_path(path)
@@ -18,7 +19,7 @@ module SharedTools
18
19
  # Verify the path exists and is a directory
19
20
  unless File.directory?(absolute_path)
20
21
  error_msg = "Path does not exist or is not a directory: #{path}"
21
- logger.error(error_msg)
22
+ RubyLLM.logger.error(error_msg)
22
23
  return { error: error_msg }
23
24
  end
24
25
 
@@ -37,10 +38,10 @@ module SharedTools
37
38
  end
38
39
  end
39
40
 
40
- logger.debug("Found #{formatted_files.size} files/directories (including #{hidden_files.size} hidden)")
41
+ RubyLLM.logger.debug("Found #{formatted_files.size} files/directories (including #{hidden_files.size} hidden)")
41
42
  formatted_files
42
43
  rescue => e
43
- logger.error("Failed to list files in '#{path}': #{e.message}")
44
+ RubyLLM.logger.error("Failed to list files in '#{path}': #{e.message}")
44
45
  { error: e.message }
45
46
  end
46
47
  end
@@ -2,12 +2,12 @@
2
2
  # Credit: https://max.engineer/giant-pdf-llm
3
3
 
4
4
  require "pdf-reader"
5
-
6
- require("ruby_llm") unless defined?(RubyLLM)
7
- require("shared_tools") unless defined?(SharedTools)
5
+ require_relative '../../shared_tools'
8
6
 
9
7
  module SharedTools
10
- class PdfPageReader < RubyLLM::Tool
8
+ verify_gem :ruby_llm
9
+
10
+ class PdfPageReader < ::RubyLLM::Tool
11
11
 
12
12
  description "Read the text of any set of pages from a PDF document."
13
13
  param :page_numbers,
@@ -16,21 +16,21 @@ module SharedTools
16
16
  desc: "Path to the PDF document."
17
17
 
18
18
  def execute(page_numbers:, doc_path:)
19
- logger.info("Reading PDF: #{doc_path}, pages: #{page_numbers}")
19
+ RubyLLM.logger.info("Reading PDF: #{doc_path}, pages: #{page_numbers}")
20
20
 
21
21
  begin
22
22
  @doc ||= PDF::Reader.new(doc_path)
23
- logger.debug("PDF loaded successfully, total pages: #{@doc.pages.size}")
23
+ RubyLLM.logger.debug("PDF loaded successfully, total pages: #{@doc.pages.size}")
24
24
 
25
25
  page_numbers = page_numbers.split(",").map { |num| num.strip.to_i }
26
- logger.debug("Processing pages: #{page_numbers.join(", ")}")
26
+ RubyLLM.logger.debug("Processing pages: #{page_numbers.join(", ")}")
27
27
 
28
28
  # Validate page numbers
29
29
  total_pages = @doc.pages.size
30
30
  invalid_pages = page_numbers.select { |num| num < 1 || num > total_pages }
31
31
 
32
32
  if invalid_pages.any?
33
- logger.warn("Invalid page numbers requested: #{invalid_pages.join(", ")}. Document has #{total_pages} pages.")
33
+ RubyLLM.logger.warn("Invalid page numbers requested: #{invalid_pages.join(", ")}. Document has #{total_pages} pages.")
34
34
  end
35
35
 
36
36
  # Filter valid pages and map to content
@@ -42,15 +42,15 @@ module SharedTools
42
42
  requested_pages: page_numbers,
43
43
  invalid_pages: invalid_pages,
44
44
  pages: pages.map { |num, p|
45
- logger.debug("Extracted text from page #{num} (#{p&.text&.bytesize || 0} bytes)")
45
+ RubyLLM.logger.debug("Extracted text from page #{num} (#{p&.text&.bytesize || 0} bytes)")
46
46
  { page: num, text: p&.text }
47
47
  },
48
48
  }
49
49
 
50
- logger.info("Successfully extracted #{pages.size} pages from PDF")
50
+ RubyLLM.logger.info("Successfully extracted #{pages.size} pages from PDF")
51
51
  result
52
52
  rescue => e
53
- logger.error("Failed to read PDF '#{doc_path}': #{e.message}")
53
+ RubyLLM.logger.error("Failed to read PDF '#{doc_path}': #{e.message}")
54
54
  { error: e.message }
55
55
  end
56
56
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ verify_gem :ruby_llm
7
+
8
+ class PythonEval < ::RubyLLM::Tool
9
+
10
+ description <<~DESCRIPTION
11
+ Execute Python source code safely and return the result.
12
+
13
+ This tool evaluates Python code by writing it to a temporary file
14
+ and executing it with the python3 command, capturing both stdout
15
+ and the final expression result.
16
+
17
+ WARNING: This tool executes arbitrary Python code. Use with caution.
18
+ NOTE: Requires python3 to be available in the system PATH.
19
+ DESCRIPTION
20
+ param :code, desc: "The Python code to execute"
21
+
22
+ def execute(code:)
23
+ RubyLLM.logger.info("Requesting permission to execute Python code")
24
+
25
+ if code.strip.empty?
26
+ error_msg = "Python code cannot be empty"
27
+ RubyLLM.logger.error(error_msg)
28
+ return { error: error_msg }
29
+ end
30
+
31
+ # Show user the code and ask for confirmation
32
+ puts "AI wants to execute the following Python code:"
33
+ puts "=" * 50
34
+ puts code
35
+ puts "=" * 50
36
+ print "Do you want to execute it? (y/n) "
37
+ response = gets.chomp.downcase
38
+
39
+ unless response == "y"
40
+ RubyLLM.logger.warn("User declined to execute the Python code")
41
+ return { error: "User declined to execute the Python code" }
42
+ end
43
+
44
+ RubyLLM.logger.info("Executing Python code")
45
+
46
+ begin
47
+ require 'tempfile'
48
+ require 'open3'
49
+ require 'json'
50
+
51
+ # Create a Python script that captures both output and result
52
+ python_script = create_python_wrapper(code)
53
+
54
+ # Write to temporary file
55
+ temp_file = Tempfile.new(['python_eval', '.py'])
56
+ temp_file.write(python_script)
57
+ temp_file.flush
58
+
59
+ # Execute the Python script
60
+ stdout, stderr, status = Open3.capture3("python3", temp_file.path)
61
+
62
+ temp_file.close
63
+ temp_file.unlink
64
+
65
+ if status.success?
66
+ RubyLLM.logger.debug("Python code execution completed successfully")
67
+ parse_python_output(stdout)
68
+ else
69
+ RubyLLM.logger.error("Python code execution failed: #{stderr}")
70
+ {
71
+ error: stderr.strip,
72
+ success: false
73
+ }
74
+ end
75
+ rescue => e
76
+ RubyLLM.logger.error("Failed to execute Python code: #{e.message}")
77
+ {
78
+ error: e.message,
79
+ backtrace: e.backtrace&.first(5),
80
+ success: false
81
+ }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def create_python_wrapper(user_code)
88
+ require 'base64'
89
+ encoded_code = Base64.strict_encode64(user_code)
90
+
91
+ <<~PYTHON
92
+ import sys
93
+ import json
94
+ import io
95
+ import base64
96
+ from contextlib import redirect_stdout
97
+
98
+ # Decode the user code
99
+ user_code = base64.b64decode('#{encoded_code}').decode('utf-8')
100
+
101
+ # Capture stdout
102
+ captured_output = io.StringIO()
103
+
104
+ try:
105
+ with redirect_stdout(captured_output):
106
+ # Handle compound statements (semicolon-separated)
107
+ if ';' in user_code and not user_code.strip().startswith('for ') and not user_code.strip().startswith('if '):
108
+ # Split by semicolon, execute all but last, eval the last
109
+ parts = [part.strip() for part in user_code.split(';') if part.strip()]
110
+ if len(parts) > 1:
111
+ for part in parts[:-1]:
112
+ exec(part)
113
+ # Try to eval the last part
114
+ try:
115
+ result = eval(parts[-1])
116
+ except SyntaxError:
117
+ exec(parts[-1])
118
+ result = None
119
+ else:
120
+ # Single part, try eval then exec
121
+ try:
122
+ result = eval(parts[0])
123
+ except SyntaxError:
124
+ exec(parts[0])
125
+ result = None
126
+ else:
127
+ # Try to evaluate as expression first
128
+ try:
129
+ result = eval(user_code)
130
+ except SyntaxError:
131
+ # If not an expression, execute as statement
132
+ exec(user_code)
133
+ result = None
134
+
135
+ output = captured_output.getvalue()
136
+
137
+ # Prepare result for JSON serialization
138
+ try:
139
+ json.dumps(result) # Test if result is JSON serializable
140
+ serializable_result = result
141
+ except (TypeError, ValueError):
142
+ serializable_result = str(result)
143
+
144
+ result_data = {
145
+ "success": True,
146
+ "result": serializable_result,
147
+ "output": output if output else None,
148
+ "python_type": type(result).__name__
149
+ }
150
+
151
+ print("PYTHON_EVAL_RESULT:", json.dumps(result_data))
152
+
153
+ except Exception as e:
154
+ error_data = {
155
+ "success": False,
156
+ "error": str(e),
157
+ "error_type": type(e).__name__
158
+ }
159
+ print("PYTHON_EVAL_RESULT:", json.dumps(error_data))
160
+ PYTHON
161
+ end
162
+
163
+ def parse_python_output(stdout)
164
+ lines = stdout.split("\n")
165
+ result_line = lines.find { |line| line.start_with?("PYTHON_EVAL_RESULT:") }
166
+
167
+ if result_line
168
+ json_data = result_line.sub("PYTHON_EVAL_RESULT:", "").strip
169
+ result = JSON.parse(json_data)
170
+
171
+ # Add display formatting
172
+ if result["success"]
173
+ if result["output"].nil? || result["output"].empty?
174
+ result["display"] = result["result"].inspect
175
+ else
176
+ result_part = result["result"].nil? ? "" : "\n=> #{result["result"].inspect}"
177
+ result["display"] = result["output"] + result_part
178
+ end
179
+ end
180
+
181
+ # Convert string keys to symbols
182
+ result.transform_keys(&:to_sym)
183
+ else
184
+ {
185
+ error: "Failed to parse Python execution result",
186
+ raw_output: stdout,
187
+ success: false
188
+ }
189
+ end
190
+ rescue JSON::ParserError => e
191
+ {
192
+ error: "Failed to parse Python result as JSON: #{e.message}",
193
+ raw_output: stdout,
194
+ success: false
195
+ }
196
+ end
197
+ end
198
+ end
@@ -1,37 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("ruby_llm") unless defined?(RubyLLM)
4
- require("shared_tools") unless defined?(SharedTools)
3
+ require_relative '../../shared_tools'
5
4
 
6
5
  module SharedTools
7
- class ReadFile < RubyLLM::Tool
6
+ verify_gem :ruby_llm
7
+
8
+ class ReadFile < ::RubyLLM::Tool
8
9
 
9
10
  description "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names."
10
11
  param :path, desc: "The relative path of a file in the working directory."
11
12
 
12
13
  def execute(path:)
13
- logger.info("Reading file: #{path}")
14
+ RubyLLM.logger.info("Reading file: #{path}")
14
15
 
15
16
  # Handle both relative and absolute paths consistently
16
17
  absolute_path = File.absolute_path(path)
17
18
 
18
19
  if File.directory?(absolute_path)
19
20
  error_msg = "Path is a directory, not a file: #{path}"
20
- logger.error(error_msg)
21
+ RubyLLM.logger.error(error_msg)
21
22
  return { error: error_msg }
22
23
  end
23
24
 
24
25
  unless File.exist?(absolute_path)
25
26
  error_msg = "File does not exist: #{path}"
26
- logger.error(error_msg)
27
+ RubyLLM.logger.error(error_msg)
27
28
  return { error: error_msg }
28
29
  end
29
30
 
30
31
  content = File.read(absolute_path)
31
- logger.debug("Successfully read #{content.bytesize} bytes from #{path}")
32
+ RubyLLM.logger.debug("Successfully read #{content.bytesize} bytes from #{path}")
32
33
  content
33
34
  rescue => e
34
- logger.error("Failed to read file '#{path}': #{e.message}")
35
+ RubyLLM.logger.error("Failed to read file '#{path}': #{e.message}")
35
36
  { error: e.message }
36
37
  end
37
38
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ verify_gem :ruby_llm
7
+
8
+ class RubyEval < ::RubyLLM::Tool
9
+
10
+ description <<~DESCRIPTION
11
+ Execute Ruby source code safely and return the result.
12
+
13
+ This tool evaluates Ruby code in a sandboxed context and returns
14
+ the result of the last expression or any output produced.
15
+
16
+ WARNING: This tool executes arbitrary Ruby code. Use with caution.
17
+ DESCRIPTION
18
+ param :code, desc: "The Ruby code to execute"
19
+
20
+ def execute(code:)
21
+ RubyLLM.logger.info("Requesting permission to execute Ruby code")
22
+
23
+ if code.strip.empty?
24
+ error_msg = "Ruby code cannot be empty"
25
+ RubyLLM.logger.error(error_msg)
26
+ return { error: error_msg }
27
+ end
28
+
29
+ # Show user the code and ask for confirmation
30
+ puts "AI wants to execute the following Ruby code:"
31
+ puts "=" * 50
32
+ puts code
33
+ puts "=" * 50
34
+ print "Do you want to execute it? (y/n) "
35
+ response = gets.chomp.downcase
36
+
37
+ unless response == "y"
38
+ RubyLLM.logger.warn("User declined to execute the Ruby code")
39
+ return { error: "User declined to execute the Ruby code" }
40
+ end
41
+
42
+ RubyLLM.logger.info("Executing Ruby code")
43
+
44
+ # Capture both stdout and the result of evaluation
45
+ original_stdout = $stdout
46
+ captured_output = StringIO.new
47
+ $stdout = captured_output
48
+
49
+ begin
50
+ result = eval(code)
51
+ output = captured_output.string
52
+
53
+ RubyLLM.logger.debug("Ruby code execution completed successfully")
54
+
55
+ response = {
56
+ result: result,
57
+ output: output.empty? ? nil : output,
58
+ success: true
59
+ }
60
+
61
+ # Include both result and output in a readable format
62
+ if output.empty?
63
+ response[:display] = result.inspect
64
+ else
65
+ response[:display] = output + (result.nil? ? "" : "\n=> #{result.inspect}")
66
+ end
67
+
68
+ response
69
+ rescue SyntaxError, StandardError => e
70
+ RubyLLM.logger.error("Ruby code execution failed: #{e.message}")
71
+ {
72
+ error: e.message,
73
+ backtrace: e.backtrace&.first(5),
74
+ success: false
75
+ }
76
+ ensure
77
+ $stdout = original_stdout
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("ruby_llm") unless defined?(RubyLLM)
4
- require("shared_tools") unless defined?(SharedTools)
3
+ require_relative '../../shared_tools'
5
4
 
6
5
  module SharedTools
7
- class RunShellCommand < RubyLLM::Tool
6
+ SharedTools.verify_gem :ruby_llm
7
+
8
+ class RunShellCommand < ::RubyLLM::Tool
8
9
 
9
10
  description "Execute a shell command"
10
11
  param :command, desc: "The command to execute"
11
12
 
12
13
  def execute(command:)
13
- logger.info("Requesting permission to execute command: '#{command}'")
14
+ RubyLLM.logger.info("Requesting permission to execute command: '#{command}'")
14
15
 
15
16
  if command.strip.empty?
16
17
  error_msg = "Command cannot be empty"
17
- logger.error(error_msg)
18
+ RubyLLM.logger.error(error_msg)
18
19
  return { error: error_msg }
19
20
  end
20
21
 
@@ -24,25 +25,25 @@ module SharedTools
24
25
  response = gets.chomp.downcase
25
26
 
26
27
  unless response == "y"
27
- logger.warn("User declined to execute the command: '#{command}'")
28
+ RubyLLM.logger.warn("User declined to execute the command: '#{command}'")
28
29
  return { error: "User declined to execute the command" }
29
30
  end
30
31
 
31
- logger.info("Executing command: '#{command}'")
32
+ RubyLLM.logger.info("Executing command: '#{command}'")
32
33
 
33
34
  # Use Open3 for safer command execution with proper error handling
34
35
  require "open3"
35
36
  stdout, stderr, status = Open3.capture3(command)
36
37
 
37
38
  if status.success?
38
- logger.debug("Command execution completed successfully with #{stdout.bytesize} bytes of output")
39
+ RubyLLM.logger.debug("Command execution completed successfully with #{stdout.bytesize} bytes of output")
39
40
  { stdout: stdout, exit_status: status.exitstatus }
40
41
  else
41
- logger.warn("Command execution failed with exit code #{status.exitstatus}: #{stderr}")
42
+ RubyLLM.logger.warn("Command execution failed with exit code #{status.exitstatus}: #{stderr}")
42
43
  { error: "Command failed with exit code #{status.exitstatus}", stderr: stderr, exit_status: status.exitstatus }
43
44
  end
44
45
  rescue => e
45
- logger.error("Command execution failed: #{e.message}")
46
+ RubyLLM.logger.error("Command execution failed: #{e.message}")
46
47
  { error: e.message }
47
48
  end
48
49
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require("ruby_llm") unless defined?(RubyLLM)
4
- require("shared_tools") unless defined?(SharedTools)
3
+ SharedTools.verify_gem :ruby_llm
5
4
 
6
- require_relative "ruby_llm/edit_file"
7
- require_relative "ruby_llm/list_files"
8
- require_relative "ruby_llm/pdf_page_reader"
9
- require_relative "ruby_llm/read_file"
10
- require_relative "ruby_llm/run_shell_command"
5
+ Dir.glob(File.join(__dir__, "ruby_llm", "*.rb")).each do |file|
6
+ require file
7
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedTools
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/shared_tools.rb CHANGED
@@ -1,46 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "shared_tools/version"
3
+ require "zeitwerk"
4
+ loader = Zeitwerk::Loader.for_gem
5
+ # Ignore aggregate loader files that don't define constants
6
+ loader.ignore("#{__dir__}/shared_tools/ruby_llm.rb")
7
+ loader.ignore("#{__dir__}/shared_tools/llm_rb.rb")
8
+ loader.ignore("#{__dir__}/shared_tools/omniai.rb")
9
+ loader.setup
4
10
 
5
11
  module SharedTools
6
- class << self
7
- # def included(base)
8
- # base.extend(ClassMethods)
9
-
10
- # if base.is_a?(Class)
11
- # base.class_eval do
12
- # include InstanceMethods
13
- # end
14
- # else
15
- # base.module_eval do
16
- # def self.included(sub_base)
17
- # sub_base.include(SharedTools)
18
- # end
19
- # end
20
- # end
21
- # end
22
-
23
- # def extended(object)
24
- # object.extend(ClassMethods)
25
- # end
12
+ @st_supported_gems = [:ruby_llm, :llm_rb, :omniai]
26
13
 
27
- # Hook to automatically inject logger into RubyLLM::Tool subclasses
28
- def const_added(const_name)
29
- const = const_get(const_name)
30
-
31
- if const.is_a?(Class) && defined?(RubyLLM::Tool) && const < RubyLLM::Tool
32
- const.class_eval do
33
- def logger
34
- SharedTools.logger
35
- end
14
+ class << self
15
+ def detected_gem
16
+ return :ruby_llm if defined?(::RubyLLM::Tool)
17
+ return :llm_rb if defined?(::LLM) || defined?(::Llm)
18
+ return :omniai if defined?(::OmniAI) || defined?(::Omniai)
19
+ nil
20
+ end
36
21
 
37
- def self.logger
38
- SharedTools.logger
39
- end
40
- end
41
- end
22
+ def verify_gem(a_symbol)
23
+ loaded = a_symbol == detected_gem
24
+ return true if loaded
25
+ raise "SharedTools: Please require '#{a_symbol}' gem before requiring 'shared_tools'."
42
26
  end
43
27
  end
44
- end
45
28
 
46
- require "shared_tools/core"
29
+ if detected_gem.nil?
30
+ warn "⚠️ SharedTools: No supported LLM provider API gem detected. Please require one of: #{@st_supported_gems.join(', ')} before requiring shared_tools."
31
+ end
32
+ end
metadata CHANGED
@@ -1,28 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shared_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - MadBomber Team
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: logger
13
+ name: zeitwerk
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: '2.6'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: '2.6'
26
+ - !ruby/object:Gem::Dependency
27
+ name: llm.rb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: omniai
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: pdf-reader
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +93,20 @@ dependencies:
65
93
  - - "~>"
66
94
  - !ruby/object:Gem::Version
67
95
  version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: debug_me
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
68
110
  - !ruby/object:Gem::Dependency
69
111
  name: rake
70
112
  requirement: !ruby/object:Gem::Requirement
@@ -93,8 +135,9 @@ dependencies:
93
135
  - - "~>"
94
136
  - !ruby/object:Gem::Version
95
137
  version: '3.0'
96
- description: SharedTools provides common functionality including configurable logging
97
- and LLM tools
138
+ description: |
139
+ SharedTools provides a collection of reusable common tools (aka callback functions)
140
+ for Ruby applications using various LLM-provider API gems.
98
141
  email:
99
142
  - example@example.com
100
143
  executables: []
@@ -105,12 +148,16 @@ files:
105
148
  - LICENSE
106
149
  - README.md
107
150
  - lib/shared_tools.rb
108
- - lib/shared_tools/core.rb
151
+ - lib/shared_tools/llm_rb.rb
152
+ - lib/shared_tools/llm_rb/run_shell_command.rb
153
+ - lib/shared_tools/omniai.rb
109
154
  - lib/shared_tools/ruby_llm.rb
110
155
  - lib/shared_tools/ruby_llm/edit_file.rb
111
156
  - lib/shared_tools/ruby_llm/list_files.rb
112
157
  - lib/shared_tools/ruby_llm/pdf_page_reader.rb
158
+ - lib/shared_tools/ruby_llm/python_eval.rb
113
159
  - lib/shared_tools/ruby_llm/read_file.rb
160
+ - lib/shared_tools/ruby_llm/ruby_eval.rb
114
161
  - lib/shared_tools/ruby_llm/run_shell_command.rb
115
162
  - lib/shared_tools/version.rb
116
163
  homepage: https://github.com/madbomber/shared_tools
@@ -127,7 +174,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
174
  requirements:
128
175
  - - ">="
129
176
  - !ruby/object:Gem::Version
130
- version: 2.7.0
177
+ version: 3.3.0
131
178
  required_rubygems_version: !ruby/object:Gem::Requirement
132
179
  requirements:
133
180
  - - ">="
@@ -136,5 +183,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
183
  requirements: []
137
184
  rubygems_version: 3.6.9
138
185
  specification_version: 4
139
- summary: A collection of shared tools and utilities for Ruby applications
186
+ summary: Shared utilities and AI tools for Ruby applications with configurable logging
140
187
  test_files: []
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "logger"
4
- require "singleton"
5
-
6
- module SharedTools
7
- DEFAULT_LOG_LEVEL = Logger::INFO
8
-
9
- module InstanceMethods
10
- # @return [Logger] The shared logger instance
11
- def logger
12
- SharedTools.logger
13
- end
14
- end
15
-
16
- module ClassMethods
17
- # @return [Logger] The shared logger instance
18
- def logger
19
- SharedTools.logger
20
- end
21
- end
22
-
23
-
24
- class LoggerConfiguration
25
- include Singleton
26
-
27
- attr_accessor :level, :log_device, :formatter
28
-
29
- def initialize
30
- @level = DEFAULT_LOG_LEVEL
31
- @log_device = STDOUT
32
- @formatter = nil
33
- end
34
-
35
-
36
- def apply_to(logger)
37
- logger.level = @level
38
- logger.formatter = @formatter if @formatter
39
- end
40
- end
41
-
42
-
43
- class << self
44
- # @return [Logger] The configured logger instance
45
- def logger
46
- @logger ||= create_logger
47
- end
48
-
49
- # @param new_logger [Logger] A Logger instance to use
50
- def logger=(new_logger)
51
- @logger = new_logger
52
- end
53
-
54
-
55
- # @example
56
- # SharedTools.configure_logger do |config|
57
- # config.level = Logger::DEBUG
58
- # config.log_device = 'logs/application.log'
59
- # config.formatter = proc { |severity, time, progname, msg| "[#{time}] #{severity} - #{msg}\n" }
60
- # end
61
- def configure_logger
62
- yield LoggerConfiguration.instance
63
-
64
-
65
- if @logger
66
- @logger = create_logger
67
- end
68
- end
69
-
70
- private
71
-
72
- # @return [Logger] A newly configured logger
73
- def create_logger
74
- config = LoggerConfiguration.instance
75
- logger = Logger.new(config.log_device)
76
- config.apply_to(logger)
77
- logger
78
- end
79
- end
80
-
81
-
82
- # module LoggingSupport
83
- # def self.included(base)
84
- # base.include(SharedTools)
85
- # end
86
- # end
87
- end