shared_tools 0.1.3 → 0.2.1

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.
@@ -0,0 +1,109 @@
1
+ # error_handling_tool.rb - Comprehensive error handling
2
+ require 'ruby_llm/tool'
3
+ require 'securerandom'
4
+
5
+ module Tools
6
+ class RobustTool < RubyLLM::Tool
7
+ def self.name = 'robust_tool'
8
+
9
+ description <<~DESCRIPTION
10
+ Reference tool demonstrating comprehensive error handling patterns and resilience strategies
11
+ for robust tool development. This tool showcases best practices for handling different
12
+ types of errors including validation errors, network failures, authorization issues,
13
+ and general exceptions. It implements retry mechanisms with exponential backoff,
14
+ proper resource cleanup, detailed error categorization, and user-friendly error messages.
15
+ Perfect as a template for building production-ready tools that need to handle
16
+ various failure scenarios gracefully.
17
+ DESCRIPTION
18
+
19
+ def execute(**params)
20
+ begin
21
+ validate_preconditions(params)
22
+ result = perform_operation(params)
23
+ validate_postconditions(result)
24
+
25
+ {
26
+ success: true,
27
+ result: result,
28
+ metadata: operation_metadata
29
+ }
30
+ rescue ValidationError => e
31
+ handle_validation_error(e, params)
32
+ rescue NetworkError => e
33
+ handle_network_error(e, params)
34
+ rescue AuthorizationError => e
35
+ handle_authorization_error(e, params)
36
+ rescue StandardError => e
37
+ handle_general_error(e, params)
38
+ ensure
39
+ cleanup_resources
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def validate_preconditions(params)
46
+ # Check all preconditions before execution
47
+ end
48
+
49
+ def perform_operation(params)
50
+ # Main operation logic with retry mechanism
51
+ retry_count = 0
52
+ max_retries = 3
53
+
54
+ begin
55
+ # Operation implementation
56
+ rescue RetryableError => e
57
+ retry_count += 1
58
+ if retry_count <= max_retries
59
+ sleep(2 ** retry_count) # Exponential backoff
60
+ retry
61
+ else
62
+ raise e
63
+ end
64
+ end
65
+ end
66
+
67
+ def handle_validation_error(error, params)
68
+ {
69
+ success: false,
70
+ error_type: "validation",
71
+ error: error.message,
72
+ suggestions: error.suggestions,
73
+ provided_params: params.keys
74
+ }
75
+ end
76
+
77
+ def handle_network_error(error, params)
78
+ {
79
+ success: false,
80
+ error_type: "network",
81
+ error: "Network operation failed",
82
+ retry_suggested: true,
83
+ retry_after: 30
84
+ }
85
+ end
86
+
87
+ def handle_authorization_error(error, params)
88
+ {
89
+ success: false,
90
+ error_type: "authorization",
91
+ error: "Access denied",
92
+ documentation_url: "https://docs.example.com/auth"
93
+ }
94
+ end
95
+
96
+ def handle_general_error(error, params)
97
+ {
98
+ success: false,
99
+ error_type: "general",
100
+ error: error.message,
101
+ support_reference: SecureRandom.uuid
102
+ }
103
+ end
104
+
105
+ def cleanup_resources
106
+ # Clean up any allocated resources
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,117 @@
1
+ # secure_tool_template.rb - Security best practices
2
+ require 'ruby_llm/tool'
3
+ require 'timeout'
4
+
5
+ module Tools
6
+ class SecureTool < RubyLLM::Tool
7
+ def self.name = 'secure_tool'
8
+
9
+ description <<~DESCRIPTION
10
+ Template tool demonstrating comprehensive security best practices for safe tool development.
11
+ This tool serves as a reference implementation for secure tool design, including input
12
+ validation, output sanitization, permission checks, rate limiting, audit logging,
13
+ timeout mechanisms, and proper error handling. It provides a complete security framework
14
+ that can be adapted for other tools that handle sensitive data or perform privileged
15
+ operations. All security violations are logged for monitoring and compliance purposes.
16
+ DESCRIPTION
17
+
18
+ # Input validation
19
+ param :user_input,
20
+ desc: <<~DESC,
21
+ User-provided input string that will be processed with comprehensive security validation.
22
+ Input is automatically sanitized and validated against multiple security criteria:
23
+ - Maximum length of 1000 characters to prevent buffer overflow attacks
24
+ - Character whitelist allowing only alphanumeric, spaces, hyphens, underscores, and dots
25
+ - Automatic removal of potentially dangerous characters and sequences
26
+ - Rate limiting to prevent abuse and denial-of-service attacks
27
+ All input validation failures are logged for security monitoring.
28
+ DESC
29
+ type: :string,
30
+ required: true,
31
+ validator: ->(value) {
32
+ # Custom validation logic
33
+ raise "Input too long" if value.length > 1000
34
+ raise "Invalid characters" unless value.match?(/\A[a-zA-Z0-9\s\-_\.]+\z/)
35
+ true
36
+ }
37
+
38
+ def execute(user_input:)
39
+ begin
40
+ # 1. Sanitize inputs
41
+ sanitized_input = sanitize_input(user_input)
42
+
43
+ # 2. Validate permissions
44
+ validate_permissions
45
+
46
+ # 3. Rate limiting
47
+ check_rate_limits
48
+
49
+ # 4. Audit logging
50
+ log_tool_usage(sanitized_input)
51
+
52
+ # 5. Execute with timeout
53
+ result = execute_with_timeout(sanitized_input)
54
+
55
+ # 6. Sanitize outputs
56
+ sanitized_result = sanitize_output(result)
57
+
58
+ {
59
+ success: true,
60
+ result: sanitized_result,
61
+ executed_at: Time.now.iso8601
62
+ }
63
+ rescue SecurityError => e
64
+ log_security_violation(e, user_input)
65
+ {
66
+ success: false,
67
+ error: "Security violation: Access denied",
68
+ violation_logged: true
69
+ }
70
+ rescue => e
71
+ {
72
+ success: false,
73
+ error: "Tool execution failed: #{e.message}"
74
+ }
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def sanitize_input(input)
81
+ # Remove potentially dangerous characters
82
+ # Validate against whitelist
83
+ input.gsub(/[^\w\s\-\.]/, '')
84
+ end
85
+
86
+ def validate_permissions
87
+ # Check user permissions
88
+ # Validate environment access
89
+ # Verify resource limits
90
+ end
91
+
92
+ def check_rate_limits
93
+ # Implement rate limiting logic
94
+ end
95
+
96
+ def log_tool_usage(input)
97
+ # Audit logging for compliance
98
+ end
99
+
100
+ def execute_with_timeout(input, timeout: 30)
101
+ # Implement timeout mechanism
102
+ Timeout::timeout(timeout) do
103
+ # Actual tool logic here
104
+ end
105
+ end
106
+
107
+ def sanitize_output(output)
108
+ # Remove sensitive information from output
109
+ # Validate output format
110
+ output
111
+ end
112
+
113
+ def log_security_violation(error, input)
114
+ # Log security violations for monitoring
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,110 @@
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
@@ -0,0 +1,145 @@
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
@@ -6,6 +6,7 @@ module SharedTools
6
6
  verify_gem :ruby_llm
7
7
 
8
8
  class ListFiles < ::RubyLLM::Tool
9
+ def self.name = 'list_files'
9
10
 
10
11
  description "List files and directories at a given path. If no path is provided, lists files in the current directory."
11
12
  param :path, desc: "Optional relative path to list files from. Defaults to current directory if not provided."
@@ -0,0 +1,51 @@
1
+ # shared_tools/ruby_llm/mcp/github_mcp_server.rb
2
+ # brew install github_mcp_server
3
+
4
+ require 'debug_me'
5
+ include DebugMe
6
+
7
+ require 'ruby_llm'
8
+ require 'ruby_llm/mcp'
9
+
10
+ require_relative '../../../shared_tools'
11
+
12
+ module SharedTools
13
+ verify_gem :ruby_llm
14
+
15
+ mcp_servers << RubyLLM::MCP.client(
16
+ name: "github-mcp-server",
17
+ transport_type: :stdio,
18
+ config: {
19
+ command: "/opt/homebrew/bin/github-mcp-server", # brew install github-mcp-server
20
+ args: %w[stdio],
21
+ env: { "GITHUB_PERSONAL_ACCESS_TOKEN" => ENV.fetch('GITHUB_PERSONAL_ACCESS_TOKEN') }
22
+ }
23
+ )
24
+ end
25
+
26
+
27
+ __END__
28
+
29
+
30
+ A GitHub MCP server that handles various tools and resources.
31
+
32
+ Usage:
33
+ server [command]
34
+
35
+ Available Commands:
36
+ completion Generate the autocompletion script for the specified shell
37
+ help Help about any command
38
+ stdio Start stdio server
39
+
40
+ Flags:
41
+ --dynamic-toolsets Enable dynamic toolsets
42
+ --enable-command-logging When enabled, the server will log all command requests and responses to the log file
43
+ --export-translations Save translations to a JSON file
44
+ --gh-host string Specify the GitHub hostname (for GitHub Enterprise etc.)
45
+ -h, --help help for server
46
+ --log-file string Path to log file
47
+ --read-only Restrict the server to read-only operations
48
+ --toolsets strings An optional comma separated list of groups of tools to allow, defaults to enabling all (default [all])
49
+ -v, --version version for server
50
+
51
+ Use "server [command] --help" for more information about a command.
@@ -0,0 +1,33 @@
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
+ # CAUTION: AIA is getting an exception when trying to use this MCP client. Its returning to
7
+ # do a to_sym on a nil value. This is due to a lack of a nil guard in the
8
+ # version 0.3.1 of the ruby_llm-mpc Parameter#item_type method.
9
+ #
10
+ # NOTE: iMCP's server is a noisy little thing shooting all its log messages to STDERR.
11
+ # To silence it, redirect STDERR to /dev/null.
12
+ # If you messages then you might want to redirect STDERR to a file.
13
+ #
14
+
15
+ require 'debug_me'
16
+ include DebugMe
17
+
18
+ require 'ruby_llm'
19
+ require 'ruby_llm/mcp'
20
+
21
+ require_relative '../../../shared_tools'
22
+
23
+ module SharedTools
24
+ verify_gem :ruby_llm
25
+
26
+ mcp_servers << RubyLLM::MCP.client(
27
+ name: "imcp-server",
28
+ transport_type: :stdio,
29
+ config: {
30
+ command: "/Applications/iMCP.app/Contents/MacOS/imcp-server 2> /dev/null"
31
+ }
32
+ )
33
+ end
@@ -0,0 +1,10 @@
1
+ # shared_tools/ruby_llm/mcp.rb
2
+ # This file loads all Ruby files in the mcp directory
3
+
4
+ # Get the directory path
5
+ mcp_dir = File.join(__dir__, 'mcp')
6
+
7
+ # Load all .rb files in the mcp directory
8
+ Dir.glob(File.join(mcp_dir, '*.rb')).each do |file|
9
+ require file
10
+ end
@@ -8,6 +8,7 @@ module SharedTools
8
8
  verify_gem :ruby_llm
9
9
 
10
10
  class PdfPageReader < ::RubyLLM::Tool
11
+ def self.name = 'pdf_page_reader'
11
12
 
12
13
  description "Read the text of any set of pages from a PDF document."
13
14
  param :page_numbers,
@@ -6,6 +6,7 @@ module SharedTools
6
6
  verify_gem :ruby_llm
7
7
 
8
8
  class PythonEval < ::RubyLLM::Tool
9
+ def self.name = 'python_eval'
9
10
 
10
11
  description <<~DESCRIPTION
11
12
  Execute Python source code safely and return the result.
@@ -29,7 +30,7 @@ module SharedTools
29
30
  end
30
31
 
31
32
  # Show user the code and ask for confirmation
32
- allowed = SharedTools.execute?(tool: self.class.name, stuff: code)
33
+ allowed = SharedTools.execute?(tool: self.class.to_s, stuff: code)
33
34
 
34
35
  unless allowed
35
36
  RubyLLM.logger.warn("User declined to execute the Python code")
@@ -6,6 +6,7 @@ module SharedTools
6
6
  verify_gem :ruby_llm
7
7
 
8
8
  class ReadFile < ::RubyLLM::Tool
9
+ def self.name = 'read_file'
9
10
 
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."
11
12
  param :path, desc: "The relative path of a file in the working directory."
@@ -6,6 +6,7 @@ module SharedTools
6
6
  verify_gem :ruby_llm
7
7
 
8
8
  class RubyEval < ::RubyLLM::Tool
9
+ def self.name = 'ruby_eval'
9
10
 
10
11
  description <<~DESCRIPTION
11
12
  Execute Ruby source code safely and return the result.
@@ -27,7 +28,7 @@ module SharedTools
27
28
  end
28
29
 
29
30
  # Show user the code and ask for confirmation
30
- allowed = SharedTools.execute?(tool: self.class.name, stuff: code)
31
+ allowed = SharedTools.execute?(tool: self.class.to_s, stuff: code)
31
32
 
32
33
  unless allowed
33
34
  RubyLLM.logger.warn("User declined to execute the Ruby code")
@@ -6,6 +6,7 @@ module SharedTools
6
6
  SharedTools.verify_gem :ruby_llm
7
7
 
8
8
  class RunShellCommand < ::RubyLLM::Tool
9
+ def self.name = 'run_shell_command'
9
10
 
10
11
  description "Execute a shell command"
11
12
  param :command, desc: "The command to execute"
@@ -20,7 +21,7 @@ module SharedTools
20
21
  end
21
22
 
22
23
  # Show user the command and ask for confirmation
23
- allowed = SharedTools.execute?(tool: self.class.name, stuff: command)
24
+ allowed = SharedTools.execute?(tool: self.class.to_s, stuff: command)
24
25
 
25
26
  unless allowed
26
27
  RubyLLM.logger.warn("User declined to execute the command: '#{command}'")
@@ -4,6 +4,9 @@ require_relative '../shared_tools'
4
4
 
5
5
  SharedTools.verify_gem :ruby_llm
6
6
 
7
- Dir.glob(File.join(__dir__, "ruby_llm", "*.rb")).each do |file|
7
+ # This excludes the sub-directories and mcp.rb
8
+ Dir.glob(File.join(__dir__, "ruby_llm", "*.rb"))
9
+ .reject { |f| File.basename(f) == 'mcp.rb' }
10
+ .each do |file|
8
11
  require file
9
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedTools
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.1"
5
5
  end