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
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# weather_tool.rb - API integration example
|
|
2
|
-
require 'ruby_llm/tool'
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
|
-
module Tools
|
|
7
|
-
class WeatherTool < RubyLLM::Tool
|
|
8
|
-
def self.name = 'weather_tool'
|
|
9
|
-
|
|
10
|
-
description <<~DESCRIPTION
|
|
11
|
-
Retrieve comprehensive current weather information for any city worldwide using the OpenWeatherMap API.
|
|
12
|
-
This tool provides real-time weather data including temperature, atmospheric conditions, humidity,
|
|
13
|
-
and wind information. It supports multiple temperature units and can optionally include extended
|
|
14
|
-
forecast data. The tool requires a valid OpenWeatherMap API key to be configured in the
|
|
15
|
-
OPENWEATHER_API_KEY environment variable. All weather data is fetched in real-time and includes
|
|
16
|
-
timestamps for accuracy verification.
|
|
17
|
-
DESCRIPTION
|
|
18
|
-
|
|
19
|
-
param :city,
|
|
20
|
-
desc: <<~DESC,
|
|
21
|
-
Name of the city for weather lookup. Can include city name only (e.g., 'London')
|
|
22
|
-
or city with country code for better accuracy (e.g., 'London,UK' or 'Paris,FR').
|
|
23
|
-
For cities with common names in multiple countries, including the country code
|
|
24
|
-
is recommended to ensure accurate results. The API will attempt to find the
|
|
25
|
-
closest match if an exact match is not found.
|
|
26
|
-
DESC
|
|
27
|
-
type: :string,
|
|
28
|
-
required: true
|
|
29
|
-
|
|
30
|
-
param :units,
|
|
31
|
-
desc: <<~DESC,
|
|
32
|
-
Temperature unit system for the weather data. Options are:
|
|
33
|
-
- 'metric': Temperature in Celsius, wind speed in m/s, pressure in hPa
|
|
34
|
-
- 'imperial': Temperature in Fahrenheit, wind speed in mph, pressure in hPa
|
|
35
|
-
- 'kelvin': Temperature in Kelvin (scientific standard), wind speed in m/s
|
|
36
|
-
Default is 'metric' which is most commonly used internationally.
|
|
37
|
-
DESC
|
|
38
|
-
type: :string,
|
|
39
|
-
default: "metric",
|
|
40
|
-
enum: ["metric", "imperial", "kelvin"]
|
|
41
|
-
|
|
42
|
-
param :include_forecast,
|
|
43
|
-
desc: <<~DESC,
|
|
44
|
-
Boolean flag to include a 3-day weather forecast in addition to current conditions.
|
|
45
|
-
When set to true, the response will include forecast data with daily high/low temperatures,
|
|
46
|
-
precipitation probability, and general weather conditions for the next three days.
|
|
47
|
-
This requires additional API calls and may increase response time slightly.
|
|
48
|
-
DESC
|
|
49
|
-
type: :boolean,
|
|
50
|
-
default: false
|
|
51
|
-
|
|
52
|
-
def execute(city:, units: "metric", include_forecast: false)
|
|
53
|
-
begin
|
|
54
|
-
api_key = ENV['OPENWEATHER_API_KEY']
|
|
55
|
-
raise "OpenWeather API key not configured" unless api_key
|
|
56
|
-
|
|
57
|
-
current_weather = fetch_current_weather(city, units, api_key)
|
|
58
|
-
result = {
|
|
59
|
-
success: true,
|
|
60
|
-
city: city,
|
|
61
|
-
current: current_weather,
|
|
62
|
-
units: units,
|
|
63
|
-
timestamp: Time.now.iso8601
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if include_forecast
|
|
67
|
-
forecast_data = fetch_forecast(city, units, api_key)
|
|
68
|
-
result[:forecast] = forecast_data
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
result
|
|
72
|
-
rescue => e
|
|
73
|
-
{
|
|
74
|
-
success: false,
|
|
75
|
-
error: e.message,
|
|
76
|
-
city: city,
|
|
77
|
-
suggestion: "Verify city name and API key configuration"
|
|
78
|
-
}
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def fetch_current_weather(city, units, api_key)
|
|
85
|
-
uri = URI("https://api.openweathermap.org/data/2.5/weather")
|
|
86
|
-
params = {
|
|
87
|
-
q: city,
|
|
88
|
-
appid: api_key,
|
|
89
|
-
units: units
|
|
90
|
-
}
|
|
91
|
-
uri.query = URI.encode_www_form(params)
|
|
92
|
-
|
|
93
|
-
response = Net::HTTP.get_response(uri)
|
|
94
|
-
raise "Weather API error: #{response.code}" unless response.code == '200'
|
|
95
|
-
|
|
96
|
-
data = JSON.parse(response.body)
|
|
97
|
-
{
|
|
98
|
-
temperature: data['main']['temp'],
|
|
99
|
-
description: data['weather'][0]['description'],
|
|
100
|
-
humidity: data['main']['humidity'],
|
|
101
|
-
wind_speed: data['wind']['speed']
|
|
102
|
-
}
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def fetch_forecast(city, units, api_key)
|
|
106
|
-
# Implementation for forecast data
|
|
107
|
-
# Similar pattern to current weather
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# workflow_manager_tool.rb - Managing state across tool invocations
|
|
2
|
-
require 'ruby_llm/tool'
|
|
3
|
-
require 'securerandom'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
|
-
module Tools
|
|
7
|
-
class WorkflowManager < RubyLLM::Tool
|
|
8
|
-
def self.name = 'workflow_manager'
|
|
9
|
-
|
|
10
|
-
description <<~DESCRIPTION
|
|
11
|
-
Manage complex multi-step workflows with persistent state tracking across tool invocations.
|
|
12
|
-
This tool enables the creation and management of stateful workflows that can span multiple
|
|
13
|
-
AI interactions and tool calls. It provides workflow initialization, step-by-step execution,
|
|
14
|
-
status monitoring, and completion tracking. Each workflow maintains its state in persistent
|
|
15
|
-
storage, allowing for resumption of long-running processes and coordination between
|
|
16
|
-
multiple tools and AI interactions. Perfect for complex automation tasks that require
|
|
17
|
-
multiple stages and decision points.
|
|
18
|
-
DESCRIPTION
|
|
19
|
-
|
|
20
|
-
param :action,
|
|
21
|
-
desc: <<~DESC,
|
|
22
|
-
Workflow management action to perform:
|
|
23
|
-
- 'start': Initialize a new workflow with initial data and return workflow ID
|
|
24
|
-
- 'step': Execute the next step in an existing workflow using provided step data
|
|
25
|
-
- 'status': Check the current status and progress of an existing workflow
|
|
26
|
-
- 'complete': Mark a workflow as finished and clean up associated resources
|
|
27
|
-
Each action requires different combinations of the other parameters.
|
|
28
|
-
DESC
|
|
29
|
-
type: :string,
|
|
30
|
-
required: true,
|
|
31
|
-
enum: ["start", "step", "status", "complete"]
|
|
32
|
-
|
|
33
|
-
param :workflow_id,
|
|
34
|
-
desc: <<~DESC,
|
|
35
|
-
Unique identifier for an existing workflow. Required for 'step', 'status', and 'complete'
|
|
36
|
-
actions. This ID is returned when starting a new workflow and should be used for all
|
|
37
|
-
subsequent operations on that workflow. The ID is a UUID string that ensures
|
|
38
|
-
uniqueness across all workflow instances.
|
|
39
|
-
DESC
|
|
40
|
-
type: :string
|
|
41
|
-
|
|
42
|
-
param :step_data,
|
|
43
|
-
desc: <<~DESC,
|
|
44
|
-
Hash containing data and parameters specific to the current workflow step.
|
|
45
|
-
For 'start' action: Initial configuration and parameters for the workflow.
|
|
46
|
-
For 'step' action: Input data, parameters, and context needed for the next step.
|
|
47
|
-
The structure depends on the specific workflow type and current step requirements.
|
|
48
|
-
Can include nested hashes, arrays, and any JSON-serializable data types.
|
|
49
|
-
DESC
|
|
50
|
-
type: :hash,
|
|
51
|
-
default: {}
|
|
52
|
-
|
|
53
|
-
def execute(action:, workflow_id: nil, step_data: {})
|
|
54
|
-
case action
|
|
55
|
-
when "start"
|
|
56
|
-
start_workflow(step_data)
|
|
57
|
-
when "step"
|
|
58
|
-
process_workflow_step(workflow_id, step_data)
|
|
59
|
-
when "status"
|
|
60
|
-
get_workflow_status(workflow_id)
|
|
61
|
-
when "complete"
|
|
62
|
-
complete_workflow(workflow_id)
|
|
63
|
-
else
|
|
64
|
-
{ success: false, error: "Unknown action: #{action}" }
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
def start_workflow(initial_data)
|
|
71
|
-
workflow_id = SecureRandom.uuid
|
|
72
|
-
workflow_state = {
|
|
73
|
-
id: workflow_id,
|
|
74
|
-
status: "active",
|
|
75
|
-
steps: [],
|
|
76
|
-
created_at: Time.now.iso8601,
|
|
77
|
-
data: initial_data
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
save_workflow_state(workflow_id, workflow_state)
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
success: true,
|
|
84
|
-
workflow_id: workflow_id,
|
|
85
|
-
status: "started",
|
|
86
|
-
next_actions: suggested_next_actions(initial_data)
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def process_workflow_step(workflow_id, step_data)
|
|
91
|
-
workflow_state = load_workflow_state(workflow_id)
|
|
92
|
-
return { success: false, error: "Workflow not found" } unless workflow_state
|
|
93
|
-
|
|
94
|
-
step = {
|
|
95
|
-
step_number: workflow_state[:steps].length + 1,
|
|
96
|
-
data: step_data,
|
|
97
|
-
processed_at: Time.now.iso8601,
|
|
98
|
-
result: process_step_logic(step_data, workflow_state)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
workflow_state[:steps] << step
|
|
102
|
-
workflow_state[:updated_at] = Time.now.iso8601
|
|
103
|
-
|
|
104
|
-
save_workflow_state(workflow_id, workflow_state)
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
success: true,
|
|
108
|
-
workflow_id: workflow_id,
|
|
109
|
-
step_completed: step,
|
|
110
|
-
workflow_status: workflow_state[:status],
|
|
111
|
-
next_actions: suggested_next_actions(workflow_state)
|
|
112
|
-
}
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def save_workflow_state(workflow_id, state)
|
|
116
|
-
# Implementation for state persistence
|
|
117
|
-
# Could use files, database, or memory store
|
|
118
|
-
File.write(".workflow_#{workflow_id}.json", state.to_json)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def load_workflow_state(workflow_id)
|
|
122
|
-
# Implementation for state loading
|
|
123
|
-
file_path = ".workflow_#{workflow_id}.json"
|
|
124
|
-
return nil unless File.exist?(file_path)
|
|
125
|
-
|
|
126
|
-
JSON.parse(File.read(file_path), symbolize_names: true)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def get_workflow_status(workflow_id)
|
|
130
|
-
# Implementation for status retrieval
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def complete_workflow(workflow_id)
|
|
134
|
-
# Implementation for workflow completion
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def suggested_next_actions(workflow_state)
|
|
138
|
-
# Implementation for suggesting next actions
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def process_step_logic(step_data, workflow_state)
|
|
142
|
-
# Implementation for processing step logic
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../../shared_tools'
|
|
4
|
-
|
|
5
|
-
module SharedTools
|
|
6
|
-
verify_gem :ruby_llm
|
|
7
|
-
|
|
8
|
-
class ListFiles < ::RubyLLM::Tool
|
|
9
|
-
def self.name = 'list_files'
|
|
10
|
-
|
|
11
|
-
description "List files and directories at a given path. If no path is provided, lists files in the current directory."
|
|
12
|
-
param :path, desc: "Optional relative path to list files from. Defaults to current directory if not provided."
|
|
13
|
-
|
|
14
|
-
def execute(path: Dir.pwd)
|
|
15
|
-
RubyLLM.logger.info("Listing files in path: #{path}")
|
|
16
|
-
|
|
17
|
-
# Convert to absolute path for consistency
|
|
18
|
-
absolute_path = File.absolute_path(path)
|
|
19
|
-
|
|
20
|
-
# Verify the path exists and is a directory
|
|
21
|
-
unless File.directory?(absolute_path)
|
|
22
|
-
error_msg = "Path does not exist or is not a directory: #{path}"
|
|
23
|
-
RubyLLM.logger.error(error_msg)
|
|
24
|
-
return { error: error_msg }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Get all files including hidden ones
|
|
28
|
-
visible_files = Dir.glob(File.join(absolute_path, "*"))
|
|
29
|
-
hidden_files = Dir.glob(File.join(absolute_path, ".*"))
|
|
30
|
-
.reject { |f| f.end_with?("/.") || f.end_with?("/..") }
|
|
31
|
-
|
|
32
|
-
# Combine and format results
|
|
33
|
-
all_files = (visible_files + hidden_files).sort
|
|
34
|
-
formatted_files = all_files.map do |filename|
|
|
35
|
-
if File.directory?(filename)
|
|
36
|
-
"#{filename}/"
|
|
37
|
-
else
|
|
38
|
-
filename
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
RubyLLM.logger.debug("Found #{formatted_files.size} files/directories (including #{hidden_files.size} hidden)")
|
|
43
|
-
formatted_files
|
|
44
|
-
rescue => e
|
|
45
|
-
RubyLLM.logger.error("Failed to list files in '#{path}': #{e.message}")
|
|
46
|
-
{ error: e.message }
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# shared_tools/ruby_llm/mcp/imcp.rb
|
|
2
|
-
# iMCP is a MacOS program that provides access to notes,calendar,contacts, etc.
|
|
3
|
-
# See: https://github.com/loopwork/iMCP
|
|
4
|
-
# brew install --cask loopwork/tap/iMCP
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
require 'ruby_llm/mcp'
|
|
8
|
-
|
|
9
|
-
RubyLLM::MCP.add_client(
|
|
10
|
-
name: "imcp-server",
|
|
11
|
-
transport_type: :stdio,
|
|
12
|
-
config: {
|
|
13
|
-
command: "/Applications/iMCP.app/Contents/MacOS/imcp-server 2> /dev/null"
|
|
14
|
-
}
|
|
15
|
-
)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# shared_tools/ruby_llm/mcp.rb
|
|
2
|
-
# This file loads all Ruby files in the mcp directory
|
|
3
|
-
|
|
4
|
-
require_relative '../../shared_tools'
|
|
5
|
-
|
|
6
|
-
# Get the directory path
|
|
7
|
-
mcp_dir = File.join(__dir__, 'mcp')
|
|
8
|
-
|
|
9
|
-
# Load all .rb files in the mcp directory
|
|
10
|
-
Dir.glob(File.join(mcp_dir, '*.rb')).each do |file|
|
|
11
|
-
require file
|
|
12
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
# Credit: https://max.engineer/giant-pdf-llm
|
|
3
|
-
|
|
4
|
-
require "pdf-reader"
|
|
5
|
-
require_relative '../../shared_tools'
|
|
6
|
-
|
|
7
|
-
module SharedTools
|
|
8
|
-
verify_gem :ruby_llm
|
|
9
|
-
|
|
10
|
-
class PdfPageReader < ::RubyLLM::Tool
|
|
11
|
-
def self.name = 'pdf_page_reader'
|
|
12
|
-
|
|
13
|
-
description "Read the text of any set of pages from a PDF document."
|
|
14
|
-
param :page_numbers,
|
|
15
|
-
desc: 'Comma-separated page numbers (first page: 1). (e.g. "12, 14, 15")'
|
|
16
|
-
param :doc_path,
|
|
17
|
-
desc: "Path to the PDF document."
|
|
18
|
-
|
|
19
|
-
def execute(page_numbers:, doc_path:)
|
|
20
|
-
RubyLLM.logger.info("Reading PDF: #{doc_path}, pages: #{page_numbers}")
|
|
21
|
-
|
|
22
|
-
begin
|
|
23
|
-
@doc ||= PDF::Reader.new(doc_path)
|
|
24
|
-
RubyLLM.logger.debug("PDF loaded successfully, total pages: #{@doc.pages.size}")
|
|
25
|
-
|
|
26
|
-
page_numbers = page_numbers.split(",").map { |num| num.strip.to_i }
|
|
27
|
-
RubyLLM.logger.debug("Processing pages: #{page_numbers.join(", ")}")
|
|
28
|
-
|
|
29
|
-
# Validate page numbers
|
|
30
|
-
total_pages = @doc.pages.size
|
|
31
|
-
invalid_pages = page_numbers.select { |num| num < 1 || num > total_pages }
|
|
32
|
-
|
|
33
|
-
if invalid_pages.any?
|
|
34
|
-
RubyLLM.logger.warn("Invalid page numbers requested: #{invalid_pages.join(", ")}. Document has #{total_pages} pages.")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Filter valid pages and map to content
|
|
38
|
-
valid_pages = page_numbers.select { |num| num >= 1 && num <= total_pages }
|
|
39
|
-
pages = valid_pages.map { |num| [num, @doc.pages[num.to_i - 1]] }
|
|
40
|
-
|
|
41
|
-
result = {
|
|
42
|
-
total_pages: total_pages,
|
|
43
|
-
requested_pages: page_numbers,
|
|
44
|
-
invalid_pages: invalid_pages,
|
|
45
|
-
pages: pages.map { |num, p|
|
|
46
|
-
RubyLLM.logger.debug("Extracted text from page #{num} (#{p&.text&.bytesize || 0} bytes)")
|
|
47
|
-
{ page: num, text: p&.text }
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
RubyLLM.logger.info("Successfully extracted #{pages.size} pages from PDF")
|
|
52
|
-
result
|
|
53
|
-
rescue => e
|
|
54
|
-
RubyLLM.logger.error("Failed to read PDF '#{doc_path}': #{e.message}")
|
|
55
|
-
{ error: e.message }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,194 +0,0 @@
|
|
|
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
|
-
def self.name = 'python_eval'
|
|
10
|
-
|
|
11
|
-
description <<~DESCRIPTION
|
|
12
|
-
Execute Python source code safely and return the result.
|
|
13
|
-
|
|
14
|
-
This tool evaluates Python code by writing it to a temporary file
|
|
15
|
-
and executing it with the python3 command, capturing both stdout
|
|
16
|
-
and the final expression result.
|
|
17
|
-
|
|
18
|
-
WARNING: This tool executes arbitrary Python code. Use with caution.
|
|
19
|
-
NOTE: Requires python3 to be available in the system PATH.
|
|
20
|
-
DESCRIPTION
|
|
21
|
-
param :code, desc: "The Python code to execute"
|
|
22
|
-
|
|
23
|
-
def execute(code:)
|
|
24
|
-
RubyLLM.logger.info("Requesting permission to execute Python code")
|
|
25
|
-
|
|
26
|
-
if code.strip.empty?
|
|
27
|
-
error_msg = "Python code cannot be empty"
|
|
28
|
-
RubyLLM.logger.error(error_msg)
|
|
29
|
-
return { error: error_msg }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Show user the code and ask for confirmation
|
|
33
|
-
allowed = SharedTools.execute?(tool: self.class.to_s, stuff: code)
|
|
34
|
-
|
|
35
|
-
unless allowed
|
|
36
|
-
RubyLLM.logger.warn("User declined to execute the Python code")
|
|
37
|
-
return { error: "User declined to execute the Python code" }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
RubyLLM.logger.info("Executing Python code")
|
|
41
|
-
|
|
42
|
-
begin
|
|
43
|
-
require 'tempfile'
|
|
44
|
-
require 'open3'
|
|
45
|
-
require 'json'
|
|
46
|
-
|
|
47
|
-
# Create a Python script that captures both output and result
|
|
48
|
-
python_script = create_python_wrapper(code)
|
|
49
|
-
|
|
50
|
-
# Write to temporary file
|
|
51
|
-
temp_file = Tempfile.new(['python_eval', '.py'])
|
|
52
|
-
temp_file.write(python_script)
|
|
53
|
-
temp_file.flush
|
|
54
|
-
|
|
55
|
-
# Execute the Python script
|
|
56
|
-
stdout, stderr, status = Open3.capture3("python3", temp_file.path)
|
|
57
|
-
|
|
58
|
-
temp_file.close
|
|
59
|
-
temp_file.unlink
|
|
60
|
-
|
|
61
|
-
if status.success?
|
|
62
|
-
RubyLLM.logger.debug("Python code execution completed successfully")
|
|
63
|
-
parse_python_output(stdout)
|
|
64
|
-
else
|
|
65
|
-
RubyLLM.logger.error("Python code execution failed: #{stderr}")
|
|
66
|
-
{
|
|
67
|
-
error: stderr.strip,
|
|
68
|
-
success: false
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
rescue => e
|
|
72
|
-
RubyLLM.logger.error("Failed to execute Python code: #{e.message}")
|
|
73
|
-
{
|
|
74
|
-
error: e.message,
|
|
75
|
-
backtrace: e.backtrace&.first(5),
|
|
76
|
-
success: false
|
|
77
|
-
}
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
def create_python_wrapper(user_code)
|
|
84
|
-
require 'base64'
|
|
85
|
-
encoded_code = Base64.strict_encode64(user_code)
|
|
86
|
-
|
|
87
|
-
<<~PYTHON
|
|
88
|
-
import sys
|
|
89
|
-
import json
|
|
90
|
-
import io
|
|
91
|
-
import base64
|
|
92
|
-
from contextlib import redirect_stdout
|
|
93
|
-
|
|
94
|
-
# Decode the user code
|
|
95
|
-
user_code = base64.b64decode('#{encoded_code}').decode('utf-8')
|
|
96
|
-
|
|
97
|
-
# Capture stdout
|
|
98
|
-
captured_output = io.StringIO()
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
with redirect_stdout(captured_output):
|
|
102
|
-
# Handle compound statements (semicolon-separated)
|
|
103
|
-
if ';' in user_code and not user_code.strip().startswith('for ') and not user_code.strip().startswith('if '):
|
|
104
|
-
# Split by semicolon, execute all but last, eval the last
|
|
105
|
-
parts = [part.strip() for part in user_code.split(';') if part.strip()]
|
|
106
|
-
if len(parts) > 1:
|
|
107
|
-
for part in parts[:-1]:
|
|
108
|
-
exec(part)
|
|
109
|
-
# Try to eval the last part
|
|
110
|
-
try:
|
|
111
|
-
result = eval(parts[-1])
|
|
112
|
-
except SyntaxError:
|
|
113
|
-
exec(parts[-1])
|
|
114
|
-
result = None
|
|
115
|
-
else:
|
|
116
|
-
# Single part, try eval then exec
|
|
117
|
-
try:
|
|
118
|
-
result = eval(parts[0])
|
|
119
|
-
except SyntaxError:
|
|
120
|
-
exec(parts[0])
|
|
121
|
-
result = None
|
|
122
|
-
else:
|
|
123
|
-
# Try to evaluate as expression first
|
|
124
|
-
try:
|
|
125
|
-
result = eval(user_code)
|
|
126
|
-
except SyntaxError:
|
|
127
|
-
# If not an expression, execute as statement
|
|
128
|
-
exec(user_code)
|
|
129
|
-
result = None
|
|
130
|
-
|
|
131
|
-
output = captured_output.getvalue()
|
|
132
|
-
|
|
133
|
-
# Prepare result for JSON serialization
|
|
134
|
-
try:
|
|
135
|
-
json.dumps(result) # Test if result is JSON serializable
|
|
136
|
-
serializable_result = result
|
|
137
|
-
except (TypeError, ValueError):
|
|
138
|
-
serializable_result = str(result)
|
|
139
|
-
|
|
140
|
-
result_data = {
|
|
141
|
-
"success": True,
|
|
142
|
-
"result": serializable_result,
|
|
143
|
-
"output": output if output else None,
|
|
144
|
-
"python_type": type(result).__name__
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
print("PYTHON_EVAL_RESULT:", json.dumps(result_data))
|
|
148
|
-
|
|
149
|
-
except Exception as e:
|
|
150
|
-
error_data = {
|
|
151
|
-
"success": False,
|
|
152
|
-
"error": str(e),
|
|
153
|
-
"error_type": type(e).__name__
|
|
154
|
-
}
|
|
155
|
-
print("PYTHON_EVAL_RESULT:", json.dumps(error_data))
|
|
156
|
-
PYTHON
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def parse_python_output(stdout)
|
|
160
|
-
lines = stdout.split("\n")
|
|
161
|
-
result_line = lines.find { |line| line.start_with?("PYTHON_EVAL_RESULT:") }
|
|
162
|
-
|
|
163
|
-
if result_line
|
|
164
|
-
json_data = result_line.sub("PYTHON_EVAL_RESULT:", "").strip
|
|
165
|
-
result = JSON.parse(json_data)
|
|
166
|
-
|
|
167
|
-
# Add display formatting
|
|
168
|
-
if result["success"]
|
|
169
|
-
if result["output"].nil? || result["output"].empty?
|
|
170
|
-
result["display"] = result["result"].inspect
|
|
171
|
-
else
|
|
172
|
-
result_part = result["result"].nil? ? "" : "\n=> #{result["result"].inspect}"
|
|
173
|
-
result["display"] = result["output"] + result_part
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Convert string keys to symbols
|
|
178
|
-
result.transform_keys(&:to_sym)
|
|
179
|
-
else
|
|
180
|
-
{
|
|
181
|
-
error: "Failed to parse Python execution result",
|
|
182
|
-
raw_output: stdout,
|
|
183
|
-
success: false
|
|
184
|
-
}
|
|
185
|
-
end
|
|
186
|
-
rescue JSON::ParserError => e
|
|
187
|
-
{
|
|
188
|
-
error: "Failed to parse Python result as JSON: #{e.message}",
|
|
189
|
-
raw_output: stdout,
|
|
190
|
-
success: false
|
|
191
|
-
}
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../../shared_tools'
|
|
4
|
-
|
|
5
|
-
module SharedTools
|
|
6
|
-
verify_gem :ruby_llm
|
|
7
|
-
|
|
8
|
-
class ReadFile < ::RubyLLM::Tool
|
|
9
|
-
def self.name = 'read_file'
|
|
10
|
-
|
|
11
|
-
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."
|
|
12
|
-
param :path, desc: "The relative path of a file in the working directory."
|
|
13
|
-
|
|
14
|
-
def execute(path:)
|
|
15
|
-
RubyLLM.logger.info("Reading file: #{path}")
|
|
16
|
-
|
|
17
|
-
# Handle both relative and absolute paths consistently
|
|
18
|
-
absolute_path = File.absolute_path(path)
|
|
19
|
-
|
|
20
|
-
if File.directory?(absolute_path)
|
|
21
|
-
error_msg = "Path is a directory, not a file: #{path}"
|
|
22
|
-
RubyLLM.logger.error(error_msg)
|
|
23
|
-
return { error: error_msg }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
unless File.exist?(absolute_path)
|
|
27
|
-
error_msg = "File does not exist: #{path}"
|
|
28
|
-
RubyLLM.logger.error(error_msg)
|
|
29
|
-
return { error: error_msg }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
content = File.read(absolute_path)
|
|
33
|
-
RubyLLM.logger.debug("Successfully read #{content.bytesize} bytes from #{path}")
|
|
34
|
-
content
|
|
35
|
-
rescue => e
|
|
36
|
-
RubyLLM.logger.error("Failed to read file '#{path}': #{e.message}")
|
|
37
|
-
{ error: e.message }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|