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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +594 -42
- data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +20 -3
- data/lib/shared_tools/mcp/imcp.rb +28 -0
- data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
- data/lib/shared_tools/mcp.rb +24 -0
- data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
- data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
- data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
- data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
- data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
- data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
- data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
- data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
- data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
- data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
- data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
- data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
- data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
- data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
- data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
- data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
- data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
- data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
- data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
- data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
- data/lib/shared_tools/tools/browser.rb +27 -0
- data/lib/shared_tools/tools/browser_tool.rb +255 -0
- data/lib/shared_tools/tools/calculator_tool.rb +169 -0
- data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
- data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
- data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
- data/lib/shared_tools/tools/computer.rb +21 -0
- data/lib/shared_tools/tools/computer_tool.rb +207 -0
- data/lib/shared_tools/tools/data_science_kit.rb +707 -0
- data/lib/shared_tools/tools/database/base_driver.rb +17 -0
- data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
- data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
- data/lib/shared_tools/tools/database.rb +9 -0
- data/lib/shared_tools/tools/database_query_tool.rb +313 -0
- data/lib/shared_tools/tools/database_tool.rb +99 -0
- data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
- data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
- data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
- data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
- data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
- data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
- data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
- data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
- data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
- data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
- data/lib/shared_tools/tools/disk.rb +17 -0
- data/lib/shared_tools/tools/disk_tool.rb +132 -0
- data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
- data/lib/shared_tools/tools/doc.rb +8 -0
- data/lib/shared_tools/tools/doc_tool.rb +109 -0
- data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
- data/lib/shared_tools/tools/docker.rb +8 -0
- data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
- data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
- data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
- data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
- data/lib/shared_tools/tools/eval.rb +10 -0
- data/lib/shared_tools/tools/eval_tool.rb +139 -0
- data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
- data/lib/shared_tools/tools/version.rb +7 -0
- data/lib/shared_tools/tools/weather_tool.rb +197 -0
- data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
- data/lib/shared_tools/tools.rb +16 -0
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools.rb +9 -24
- metadata +189 -68
- data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
- data/lib/shared_tools/llm_rb.rb +0 -9
- data/lib/shared_tools/omniai.rb +0 -9
- data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
- data/lib/shared_tools/raix.rb +0 -9
- data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
- data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
- data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
- data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
- data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
- data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
- data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
- data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
- data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
- data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
- data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
- data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -15
- data/lib/shared_tools/ruby_llm/mcp.rb +0 -12
- data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
- data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
- data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
- data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
- data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
- data/lib/shared_tools/ruby_llm.rb +0 -12
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module SharedTools
|
|
5
|
+
module Tools
|
|
6
|
+
module Eval
|
|
7
|
+
# @example
|
|
8
|
+
# tool = SharedTools::Tools::Eval::PythonEvalTool.new
|
|
9
|
+
# tool.execute(code: "print('Hello'); result = 2 + 2")
|
|
10
|
+
class PythonEvalTool < ::RubyLLM::Tool
|
|
11
|
+
def self.name = 'eval_python'
|
|
12
|
+
|
|
13
|
+
description <<~DESCRIPTION
|
|
14
|
+
Execute Python source code safely and return the result.
|
|
15
|
+
|
|
16
|
+
This tool evaluates Python code by writing it to a temporary file
|
|
17
|
+
and executing it with the python3 command, capturing both stdout
|
|
18
|
+
and the final expression result.
|
|
19
|
+
|
|
20
|
+
WARNING: This tool executes arbitrary Python code. Use with caution.
|
|
21
|
+
NOTE: Requires python3 to be available in the system PATH.
|
|
22
|
+
DESCRIPTION
|
|
23
|
+
|
|
24
|
+
params do
|
|
25
|
+
string :code, description: "The Python code to execute"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param logger [Logger] optional logger
|
|
29
|
+
def initialize(logger: nil)
|
|
30
|
+
@logger = logger || RubyLLM.logger
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param code [String] Python code to execute
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] execution result
|
|
36
|
+
def execute(code:)
|
|
37
|
+
@logger.info("Requesting permission to execute Python code")
|
|
38
|
+
|
|
39
|
+
if code.strip.empty?
|
|
40
|
+
error_msg = "Python code cannot be empty"
|
|
41
|
+
@logger.error(error_msg)
|
|
42
|
+
return { error: error_msg }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Show user the code and ask for confirmation
|
|
46
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: code)
|
|
47
|
+
|
|
48
|
+
unless allowed
|
|
49
|
+
@logger.warn("User declined to execute the Python code")
|
|
50
|
+
return { error: "User declined to execute the Python code" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@logger.info("Executing Python code")
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
require 'tempfile'
|
|
57
|
+
require 'open3'
|
|
58
|
+
require 'json'
|
|
59
|
+
|
|
60
|
+
# Create a Python script that captures both output and result
|
|
61
|
+
python_script = create_python_wrapper(code)
|
|
62
|
+
|
|
63
|
+
# Write to temporary file
|
|
64
|
+
temp_file = Tempfile.new(['python_eval', '.py'])
|
|
65
|
+
temp_file.write(python_script)
|
|
66
|
+
temp_file.flush
|
|
67
|
+
|
|
68
|
+
# Execute the Python script
|
|
69
|
+
stdout, stderr, status = Open3.capture3("python3", temp_file.path)
|
|
70
|
+
|
|
71
|
+
temp_file.close
|
|
72
|
+
temp_file.unlink
|
|
73
|
+
|
|
74
|
+
if status.success?
|
|
75
|
+
@logger.debug("Python code execution completed successfully")
|
|
76
|
+
parse_python_output(stdout)
|
|
77
|
+
else
|
|
78
|
+
@logger.error("Python code execution failed: #{stderr}")
|
|
79
|
+
{
|
|
80
|
+
error: stderr.strip,
|
|
81
|
+
success: false
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
rescue => e
|
|
85
|
+
@logger.error("Failed to execute Python code: #{e.message}")
|
|
86
|
+
{
|
|
87
|
+
error: e.message,
|
|
88
|
+
backtrace: e.backtrace&.first(5),
|
|
89
|
+
success: false
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def create_python_wrapper(user_code)
|
|
97
|
+
require 'base64'
|
|
98
|
+
encoded_code = Base64.strict_encode64(user_code)
|
|
99
|
+
|
|
100
|
+
<<~PYTHON
|
|
101
|
+
import sys
|
|
102
|
+
import json
|
|
103
|
+
import io
|
|
104
|
+
import base64
|
|
105
|
+
from contextlib import redirect_stdout
|
|
106
|
+
|
|
107
|
+
# Decode the user code
|
|
108
|
+
user_code = base64.b64decode('#{encoded_code}').decode('utf-8')
|
|
109
|
+
|
|
110
|
+
# Capture stdout
|
|
111
|
+
captured_output = io.StringIO()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with redirect_stdout(captured_output):
|
|
115
|
+
# Handle compound statements (semicolon-separated)
|
|
116
|
+
if ';' in user_code and not user_code.strip().startswith('for ') and not user_code.strip().startswith('if '):
|
|
117
|
+
# Split by semicolon, execute all but last, eval the last
|
|
118
|
+
parts = [part.strip() for part in user_code.split(';') if part.strip()]
|
|
119
|
+
if len(parts) > 1:
|
|
120
|
+
for part in parts[:-1]:
|
|
121
|
+
exec(part)
|
|
122
|
+
# Try to eval the last part
|
|
123
|
+
try:
|
|
124
|
+
result = eval(parts[-1])
|
|
125
|
+
except SyntaxError:
|
|
126
|
+
exec(parts[-1])
|
|
127
|
+
result = None
|
|
128
|
+
else:
|
|
129
|
+
# Single part, try eval then exec
|
|
130
|
+
try:
|
|
131
|
+
result = eval(parts[0])
|
|
132
|
+
except SyntaxError:
|
|
133
|
+
exec(parts[0])
|
|
134
|
+
result = None
|
|
135
|
+
else:
|
|
136
|
+
# Try to evaluate as expression first
|
|
137
|
+
try:
|
|
138
|
+
result = eval(user_code)
|
|
139
|
+
except SyntaxError:
|
|
140
|
+
# If not an expression, execute as statement
|
|
141
|
+
exec(user_code)
|
|
142
|
+
result = None
|
|
143
|
+
|
|
144
|
+
output = captured_output.getvalue()
|
|
145
|
+
|
|
146
|
+
# Prepare result for JSON serialization
|
|
147
|
+
try:
|
|
148
|
+
json.dumps(result) # Test if result is JSON serializable
|
|
149
|
+
serializable_result = result
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
serializable_result = str(result)
|
|
152
|
+
|
|
153
|
+
result_data = {
|
|
154
|
+
"success": True,
|
|
155
|
+
"result": serializable_result,
|
|
156
|
+
"output": output if output else None,
|
|
157
|
+
"python_type": type(result).__name__
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print("PYTHON_EVAL_RESULT:", json.dumps(result_data))
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
error_data = {
|
|
164
|
+
"success": False,
|
|
165
|
+
"error": str(e),
|
|
166
|
+
"error_type": type(e).__name__
|
|
167
|
+
}
|
|
168
|
+
print("PYTHON_EVAL_RESULT:", json.dumps(error_data))
|
|
169
|
+
PYTHON
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def parse_python_output(stdout)
|
|
173
|
+
lines = stdout.split("\n")
|
|
174
|
+
result_line = lines.find { |line| line.start_with?("PYTHON_EVAL_RESULT:") }
|
|
175
|
+
|
|
176
|
+
if result_line
|
|
177
|
+
json_data = result_line.sub("PYTHON_EVAL_RESULT:", "").strip
|
|
178
|
+
result = JSON.parse(json_data)
|
|
179
|
+
|
|
180
|
+
# Add display formatting
|
|
181
|
+
if result["success"]
|
|
182
|
+
if result["output"].nil? || result["output"].empty?
|
|
183
|
+
result["display"] = result["result"].inspect
|
|
184
|
+
else
|
|
185
|
+
result_part = result["result"].nil? ? "" : "\n=> #{result["result"].inspect}"
|
|
186
|
+
result["display"] = result["output"] + result_part
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Convert string keys to symbols
|
|
191
|
+
result.transform_keys(&:to_sym)
|
|
192
|
+
else
|
|
193
|
+
{
|
|
194
|
+
error: "Failed to parse Python execution result",
|
|
195
|
+
raw_output: stdout,
|
|
196
|
+
success: false
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
rescue JSON::ParserError => e
|
|
200
|
+
{
|
|
201
|
+
error: "Failed to parse Python result as JSON: #{e.message}",
|
|
202
|
+
raw_output: stdout,
|
|
203
|
+
success: false
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Eval
|
|
8
|
+
# @example
|
|
9
|
+
# tool = SharedTools::Tools::Eval::RubyEvalTool.new
|
|
10
|
+
# tool.execute(code: "puts 'Hello'; 2 + 2")
|
|
11
|
+
class RubyEvalTool < ::RubyLLM::Tool
|
|
12
|
+
def self.name = 'eval_ruby'
|
|
13
|
+
|
|
14
|
+
description <<~DESCRIPTION
|
|
15
|
+
Execute Ruby source code safely and return the result.
|
|
16
|
+
|
|
17
|
+
This tool evaluates Ruby code in a sandboxed context and returns
|
|
18
|
+
the result of the last expression or any output produced.
|
|
19
|
+
|
|
20
|
+
WARNING: This tool executes arbitrary Ruby code. Use with caution.
|
|
21
|
+
DESCRIPTION
|
|
22
|
+
|
|
23
|
+
params do
|
|
24
|
+
string :code, description: "The Ruby code to execute"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param logger [Logger] optional logger
|
|
28
|
+
def initialize(logger: nil)
|
|
29
|
+
@logger = logger || RubyLLM.logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param code [String] Ruby code to execute
|
|
33
|
+
#
|
|
34
|
+
# @return [Hash] execution result
|
|
35
|
+
def execute(code:)
|
|
36
|
+
@logger.info("Requesting permission to execute Ruby code")
|
|
37
|
+
|
|
38
|
+
if code.strip.empty?
|
|
39
|
+
error_msg = "Ruby code cannot be empty"
|
|
40
|
+
@logger.error(error_msg)
|
|
41
|
+
return { error: error_msg }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Show user the code and ask for confirmation
|
|
45
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: code)
|
|
46
|
+
|
|
47
|
+
unless allowed
|
|
48
|
+
@logger.warn("User declined to execute the Ruby code")
|
|
49
|
+
return { error: "User declined to execute the Ruby code" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@logger.info("Executing Ruby code")
|
|
53
|
+
|
|
54
|
+
# Capture both stdout and the result of evaluation
|
|
55
|
+
original_stdout = $stdout
|
|
56
|
+
captured_output = StringIO.new
|
|
57
|
+
$stdout = captured_output
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
result = eval(code)
|
|
61
|
+
output = captured_output.string
|
|
62
|
+
|
|
63
|
+
@logger.debug("Ruby code execution completed successfully")
|
|
64
|
+
|
|
65
|
+
response = {
|
|
66
|
+
result: result,
|
|
67
|
+
output: output.empty? ? nil : output,
|
|
68
|
+
success: true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Include both result and output in a readable format
|
|
72
|
+
if output.empty?
|
|
73
|
+
response[:display] = result.inspect
|
|
74
|
+
else
|
|
75
|
+
response[:display] = output + (result.nil? ? "" : "\n=> #{result.inspect}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
response
|
|
79
|
+
rescue SyntaxError, StandardError => e
|
|
80
|
+
@logger.error("Ruby code execution failed: #{e.message}")
|
|
81
|
+
{
|
|
82
|
+
error: e.message,
|
|
83
|
+
backtrace: e.backtrace&.first(5),
|
|
84
|
+
success: false
|
|
85
|
+
}
|
|
86
|
+
ensure
|
|
87
|
+
$stdout = original_stdout
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module SharedTools
|
|
5
|
+
module Tools
|
|
6
|
+
module Eval
|
|
7
|
+
# @example
|
|
8
|
+
# tool = SharedTools::Tools::Eval::ShellEvalTool.new
|
|
9
|
+
# tool.execute(command: "ls -la")
|
|
10
|
+
class ShellEvalTool < ::RubyLLM::Tool
|
|
11
|
+
def self.name = 'eval_shell'
|
|
12
|
+
|
|
13
|
+
description "Execute a shell command safely and return the result."
|
|
14
|
+
|
|
15
|
+
params do
|
|
16
|
+
string :command, description: "The shell command to execute"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param logger [Logger] optional logger
|
|
20
|
+
def initialize(logger: nil)
|
|
21
|
+
@logger = logger || RubyLLM.logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param command [String] shell command to execute
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] execution result
|
|
27
|
+
def execute(command:)
|
|
28
|
+
@logger.info("Requesting permission to execute command: '#{command}'")
|
|
29
|
+
|
|
30
|
+
if command.strip.empty?
|
|
31
|
+
error_msg = "Command cannot be empty"
|
|
32
|
+
@logger.error(error_msg)
|
|
33
|
+
return { error: error_msg }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Show user the command and ask for confirmation
|
|
37
|
+
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: command)
|
|
38
|
+
|
|
39
|
+
unless allowed
|
|
40
|
+
@logger.warn("User declined to execute the command: '#{command}'")
|
|
41
|
+
return { error: "User declined to execute the command" }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@logger.info("Executing command: '#{command}'")
|
|
45
|
+
|
|
46
|
+
# Use Open3 for safer command execution with proper error handling
|
|
47
|
+
require "open3"
|
|
48
|
+
stdout, stderr, status = Open3.capture3(command)
|
|
49
|
+
|
|
50
|
+
if status.success?
|
|
51
|
+
@logger.debug("Command execution completed successfully with #{stdout.bytesize} bytes of output")
|
|
52
|
+
{ stdout: stdout, exit_status: status.exitstatus }
|
|
53
|
+
else
|
|
54
|
+
@logger.warn("Command execution failed with exit code #{status.exitstatus}: #{stderr}")
|
|
55
|
+
{ error: "Command failed with exit code #{status.exitstatus}", stderr: stderr, exit_status: status.exitstatus }
|
|
56
|
+
end
|
|
57
|
+
rescue => e
|
|
58
|
+
@logger.error("Command execution failed: #{e.message}")
|
|
59
|
+
{ error: e.message }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collection loader for all eval tools
|
|
4
|
+
# Usage: require 'shared_tools/tools/eval'
|
|
5
|
+
|
|
6
|
+
require 'shared_tools'
|
|
7
|
+
|
|
8
|
+
require_relative 'eval/python_eval_tool'
|
|
9
|
+
require_relative 'eval/ruby_eval_tool'
|
|
10
|
+
require_relative 'eval/shell_eval_tool'
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../shared_tools'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
# A tool for evaluating code in different programming languages
|
|
8
|
+
class EvalTool < ::RubyLLM::Tool
|
|
9
|
+
def self.name = 'eval_tool'
|
|
10
|
+
|
|
11
|
+
module Action
|
|
12
|
+
RUBY = "ruby"
|
|
13
|
+
PYTHON = "python"
|
|
14
|
+
SHELL = "shell"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ACTIONS = [
|
|
18
|
+
Action::RUBY,
|
|
19
|
+
Action::PYTHON,
|
|
20
|
+
Action::SHELL,
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
description <<~TEXT
|
|
24
|
+
Execute code in various programming languages (Ruby, Python, Shell).
|
|
25
|
+
|
|
26
|
+
**WARNING**: This tool executes arbitrary code. All code execution requires user authorization for security.
|
|
27
|
+
|
|
28
|
+
## Actions:
|
|
29
|
+
|
|
30
|
+
1. `#{Action::RUBY}` - Execute Ruby code
|
|
31
|
+
Required: "action": "ruby", "code": "[Ruby code to execute]"
|
|
32
|
+
|
|
33
|
+
2. `#{Action::PYTHON}` - Execute Python code
|
|
34
|
+
Required: "action": "python", "code": "[Python code to execute]"
|
|
35
|
+
Note: Requires python3 in system PATH
|
|
36
|
+
|
|
37
|
+
3. `#{Action::SHELL}` - Execute shell commands
|
|
38
|
+
Required: "action": "shell", "command": "[Shell command to execute]"
|
|
39
|
+
|
|
40
|
+
## Examples:
|
|
41
|
+
|
|
42
|
+
Execute Ruby code
|
|
43
|
+
{"action": "#{Action::RUBY}", "code": "puts 'Hello from Ruby'; 2 + 2"}
|
|
44
|
+
|
|
45
|
+
Execute Python code
|
|
46
|
+
{"action": "#{Action::PYTHON}", "code": "print('Hello from Python')\\nresult = 5 * 5\\nprint(result)"}
|
|
47
|
+
|
|
48
|
+
Execute shell command
|
|
49
|
+
{"action": "#{Action::SHELL}", "command": "ls -la"}
|
|
50
|
+
|
|
51
|
+
Calculate with Ruby
|
|
52
|
+
{"action": "#{Action::RUBY}", "code": "[1, 2, 3, 4, 5].sum"}
|
|
53
|
+
|
|
54
|
+
Data processing with Python
|
|
55
|
+
{"action": "#{Action::PYTHON}", "code": "import json\\ndata = {'name': 'test', 'value': 42}\\nprint(json.dumps(data))"}
|
|
56
|
+
|
|
57
|
+
System info with shell
|
|
58
|
+
{"action": "#{Action::SHELL}", "command": "uname -a"}
|
|
59
|
+
TEXT
|
|
60
|
+
|
|
61
|
+
params do
|
|
62
|
+
string :action, description: <<~TEXT.strip
|
|
63
|
+
The evaluation action to perform. Options:
|
|
64
|
+
* `#{Action::RUBY}`: Execute Ruby code
|
|
65
|
+
* `#{Action::PYTHON}`: Execute Python code (requires python3)
|
|
66
|
+
* `#{Action::SHELL}`: Execute shell commands
|
|
67
|
+
TEXT
|
|
68
|
+
|
|
69
|
+
string :code, description: <<~TEXT.strip, required: false
|
|
70
|
+
The code to execute. Required for the following actions:
|
|
71
|
+
* `#{Action::RUBY}`
|
|
72
|
+
* `#{Action::PYTHON}`
|
|
73
|
+
TEXT
|
|
74
|
+
|
|
75
|
+
string :command, description: <<~TEXT.strip, required: false
|
|
76
|
+
The shell command to execute. Required for the following actions:
|
|
77
|
+
* `#{Action::SHELL}`
|
|
78
|
+
TEXT
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param logger [Logger] optional logger
|
|
82
|
+
def initialize(logger: nil)
|
|
83
|
+
@logger = logger || RubyLLM.logger
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param action [String] the action to perform
|
|
87
|
+
# @param code [String, nil] code to execute (for ruby/python)
|
|
88
|
+
# @param command [String, nil] shell command to execute
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash, String] execution result
|
|
91
|
+
def execute(action:, code: nil, command: nil)
|
|
92
|
+
@logger.info("EvalTool#execute action=#{action}")
|
|
93
|
+
|
|
94
|
+
case action.to_s.downcase
|
|
95
|
+
when Action::RUBY
|
|
96
|
+
require_param!(:code, code)
|
|
97
|
+
ruby_eval_tool.execute(code: code)
|
|
98
|
+
when Action::PYTHON
|
|
99
|
+
require_param!(:code, code)
|
|
100
|
+
python_eval_tool.execute(code: code)
|
|
101
|
+
when Action::SHELL
|
|
102
|
+
require_param!(:command, command)
|
|
103
|
+
shell_eval_tool.execute(command: command)
|
|
104
|
+
else
|
|
105
|
+
{ error: "Unsupported action: #{action}. Supported actions are: #{ACTIONS.join(', ')}" }
|
|
106
|
+
end
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
@logger.error("EvalTool execution failed: #{e.message}")
|
|
109
|
+
{ error: e.message }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# @param name [Symbol]
|
|
115
|
+
# @param value [Object]
|
|
116
|
+
#
|
|
117
|
+
# @raise [ArgumentError]
|
|
118
|
+
# @return [void]
|
|
119
|
+
def require_param!(name, value)
|
|
120
|
+
raise ArgumentError, "#{name} param is required for this action" if value.nil?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @return [Eval::RubyEvalTool]
|
|
124
|
+
def ruby_eval_tool
|
|
125
|
+
@ruby_eval_tool ||= Eval::RubyEvalTool.new(logger: @logger)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [Eval::PythonEvalTool]
|
|
129
|
+
def python_eval_tool
|
|
130
|
+
@python_eval_tool ||= Eval::PythonEvalTool.new(logger: @logger)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [Eval::ShellEvalTool]
|
|
134
|
+
def shell_eval_tool
|
|
135
|
+
@shell_eval_tool ||= Eval::ShellEvalTool.new(logger: @logger)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|