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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Docker
|
|
8
|
+
# @example
|
|
9
|
+
# class ExampleTool s::Disk::BaseTool
|
|
10
|
+
# description "..."
|
|
11
|
+
# end
|
|
12
|
+
class BaseTool
|
|
13
|
+
# @example
|
|
14
|
+
# raise CaptureError.new(text: "an unknown error occurred", status: Process::Status.new(0))
|
|
15
|
+
class CaptureError < StandardError
|
|
16
|
+
# !@attribute [rw] text
|
|
17
|
+
# @return [String]
|
|
18
|
+
attr_accessor :text
|
|
19
|
+
|
|
20
|
+
# @!attribute [rw] status
|
|
21
|
+
# @return [Process::Status]
|
|
22
|
+
attr_accessor :status
|
|
23
|
+
|
|
24
|
+
# @param text [String]
|
|
25
|
+
# @param status [Process::Status]
|
|
26
|
+
def initialize(text:, status:)
|
|
27
|
+
super("[STATUS=#{status.exitstatus}] #{text}")
|
|
28
|
+
@text = text
|
|
29
|
+
@status = status
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param root [Pathname]
|
|
34
|
+
# @param logger [IO] optional
|
|
35
|
+
def initialize(root:, logger: Logger.new(IO::NULL))
|
|
36
|
+
super()
|
|
37
|
+
@root = root
|
|
38
|
+
@logger = logger
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
# @raise [CaptureError]
|
|
44
|
+
#
|
|
45
|
+
# @return [String]
|
|
46
|
+
def capture!(...)
|
|
47
|
+
text, status = Open3.capture2e(...)
|
|
48
|
+
|
|
49
|
+
raise CaptureError.new(text:, status:) unless status.success?
|
|
50
|
+
|
|
51
|
+
text
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Docker
|
|
8
|
+
# @example
|
|
9
|
+
# tool = SharedTools::Tools::Docker::ComposeRunTool.new
|
|
10
|
+
# tool.execute(service: "app", command: "rspec", args: ["spec/main_spec.rb"])
|
|
11
|
+
class ComposeRunTool < ::RubyLLM::Tool
|
|
12
|
+
def self.name = 'docker_compose_run'
|
|
13
|
+
|
|
14
|
+
description "Runs a command via Docker with arguments on the project (e.g. `rspec spec/main_spec.rb`)."
|
|
15
|
+
|
|
16
|
+
params do
|
|
17
|
+
string :service, description: "The service to run the command on (e.g. `app`).", required: false
|
|
18
|
+
string :command, description: "The command to run (e.g. `rspec`)."
|
|
19
|
+
array :args, description: "The arguments for the command.", required: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @example
|
|
23
|
+
# class ExampleTool < ::RubyLLM::Tool
|
|
24
|
+
# # ...
|
|
25
|
+
# end
|
|
26
|
+
class CaptureError < StandardError
|
|
27
|
+
attr_accessor :text
|
|
28
|
+
attr_accessor :status
|
|
29
|
+
|
|
30
|
+
# @param text [String]
|
|
31
|
+
# @param status [Process::Status]
|
|
32
|
+
def initialize(text:, status:)
|
|
33
|
+
super("[STATUS=#{status.exitstatus}] #{text}")
|
|
34
|
+
@text = text
|
|
35
|
+
@status = status
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param root [String, Pathname] optional, defaults to current directory
|
|
40
|
+
# @param logger [Logger] optional logger
|
|
41
|
+
def initialize(root: nil, logger: nil)
|
|
42
|
+
@root = root || Dir.pwd
|
|
43
|
+
@logger = logger || RubyLLM.logger
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param service [String]
|
|
47
|
+
# @param command [String]
|
|
48
|
+
# @param args [Array<String>]
|
|
49
|
+
#
|
|
50
|
+
# @return [String]
|
|
51
|
+
def execute(command:, service: "app", args: [])
|
|
52
|
+
@logger.info(%(#{self.class.name}#execute service="#{service}" command="#{command}" args=#{args.inspect}))
|
|
53
|
+
|
|
54
|
+
Dir.chdir(@root) do
|
|
55
|
+
capture!("docker", "compose", "run", "--build", "--rm", service, command, *args)
|
|
56
|
+
rescue CaptureError => e
|
|
57
|
+
@logger.info("ERROR: #{e.message}")
|
|
58
|
+
return "ERROR: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# @raise [CaptureError]
|
|
65
|
+
#
|
|
66
|
+
# @return [String]
|
|
67
|
+
def capture!(...)
|
|
68
|
+
text, status = Open3.capture2e(...)
|
|
69
|
+
|
|
70
|
+
raise CaptureError.new(text:, status:) unless status.success?
|
|
71
|
+
|
|
72
|
+
text
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# error_handling_tool.rb - Comprehensive error handling demonstration
|
|
2
|
+
require 'ruby_llm/tool'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module SharedTools
|
|
8
|
+
module Tools
|
|
9
|
+
# Custom error classes for different error scenarios
|
|
10
|
+
class ValidationError < StandardError
|
|
11
|
+
attr_reader :suggestions
|
|
12
|
+
|
|
13
|
+
def initialize(message, suggestions: [])
|
|
14
|
+
super(message)
|
|
15
|
+
@suggestions = suggestions
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class NetworkError < StandardError; end
|
|
20
|
+
class AuthorizationError < StandardError; end
|
|
21
|
+
class RetryableError < StandardError; end
|
|
22
|
+
class ResourceNotFoundError < StandardError; end
|
|
23
|
+
|
|
24
|
+
class ErrorHandlingTool < RubyLLM::Tool
|
|
25
|
+
def self.name = 'error_handling_tool'
|
|
26
|
+
|
|
27
|
+
description <<~'DESCRIPTION'
|
|
28
|
+
Reference tool demonstrating comprehensive error handling patterns and resilience strategies
|
|
29
|
+
for robust tool development. This tool showcases best practices for handling different
|
|
30
|
+
types of errors including validation errors, network failures, authorization issues,
|
|
31
|
+
and general exceptions. It implements retry mechanisms with exponential backoff,
|
|
32
|
+
proper resource cleanup, detailed error categorization, and user-friendly error messages.
|
|
33
|
+
|
|
34
|
+
This tool performs a demonstration operation (data validation and processing) that can
|
|
35
|
+
encounter various error scenarios to showcase the error handling patterns.
|
|
36
|
+
|
|
37
|
+
Error handling features:
|
|
38
|
+
- Input validation with helpful suggestions
|
|
39
|
+
- Network retry with exponential backoff
|
|
40
|
+
- Authorization checks
|
|
41
|
+
- Resource cleanup in ensure blocks
|
|
42
|
+
- Detailed error categorization
|
|
43
|
+
- Operation metadata tracking
|
|
44
|
+
- Support reference IDs for debugging
|
|
45
|
+
|
|
46
|
+
Example usage:
|
|
47
|
+
tool = SharedTools::Tools::ErrorHandlingTool.new
|
|
48
|
+
result = tool.execute(
|
|
49
|
+
operation: "validate",
|
|
50
|
+
data: {name: "test", value: 42},
|
|
51
|
+
simulate_error: nil
|
|
52
|
+
)
|
|
53
|
+
DESCRIPTION
|
|
54
|
+
|
|
55
|
+
params do
|
|
56
|
+
string :operation, description: <<~DESC.strip
|
|
57
|
+
The operation to perform. Options:
|
|
58
|
+
- 'validate': Validate data structure and content
|
|
59
|
+
- 'process': Process data with simulated network operation
|
|
60
|
+
- 'authorize': Check authorization (always succeeds unless simulating error)
|
|
61
|
+
|
|
62
|
+
This parameter demonstrates how to validate enum-like inputs.
|
|
63
|
+
DESC
|
|
64
|
+
|
|
65
|
+
object :data, description: <<~DESC.strip, required: false do
|
|
66
|
+
Data object to process. Contains the data to be validated or processed.
|
|
67
|
+
Example: {name: "example", value: 100, optional_field: "extra info"}
|
|
68
|
+
DESC
|
|
69
|
+
string :name, description: "Name or identifier of the data item. Should be at least 2 characters long.", required: false
|
|
70
|
+
number :value, description: "Numeric value to process. Must be a valid number. Negative values will generate a warning.", required: false
|
|
71
|
+
string :optional_field, description: "Any optional string field for additional data or context.", required: false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
string :simulate_error, description: <<~DESC.strip, required: false
|
|
75
|
+
Simulate a specific error type for testing error handling. Options:
|
|
76
|
+
- 'validation': Trigger validation error
|
|
77
|
+
- 'network': Trigger network error
|
|
78
|
+
- 'authorization': Trigger authorization error
|
|
79
|
+
- 'retryable': Trigger retryable error
|
|
80
|
+
- 'resource_not_found': Trigger resource not found error
|
|
81
|
+
- null/empty: Normal operation (default)
|
|
82
|
+
|
|
83
|
+
This is useful for testing error handling in client applications.
|
|
84
|
+
DESC
|
|
85
|
+
|
|
86
|
+
integer :max_retries, description: <<~DESC.strip, required: false
|
|
87
|
+
Maximum number of retries for retryable operations. Default: 3.
|
|
88
|
+
Valid range: 0-10. Set to 0 to disable retries.
|
|
89
|
+
DESC
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param logger [Logger] optional logger
|
|
93
|
+
def initialize(logger: nil)
|
|
94
|
+
@logger = logger || RubyLLM.logger
|
|
95
|
+
@resources_allocated = []
|
|
96
|
+
@operation_start_time = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Execute operation with comprehensive error handling
|
|
100
|
+
#
|
|
101
|
+
# @param operation [String] Operation to perform
|
|
102
|
+
# @param data [Hash] Data to process
|
|
103
|
+
# @param simulate_error [String, nil] Error type to simulate
|
|
104
|
+
# @param max_retries [Integer] Maximum retry attempts
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Operation result with success status
|
|
107
|
+
def execute(operation:, simulate_error: nil, max_retries: 3, **data)
|
|
108
|
+
@operation_start_time = Time.now
|
|
109
|
+
@logger.info("ErrorHandlingTool#execute operation=#{operation} simulate_error=#{simulate_error}")
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
# Validate inputs
|
|
113
|
+
validate_preconditions(operation, data, max_retries)
|
|
114
|
+
|
|
115
|
+
# Allocate resources (demonstration)
|
|
116
|
+
allocate_resources
|
|
117
|
+
|
|
118
|
+
# Perform main operation
|
|
119
|
+
result = perform_operation(operation, data, simulate_error, max_retries)
|
|
120
|
+
|
|
121
|
+
# Validate outputs
|
|
122
|
+
validate_postconditions(result)
|
|
123
|
+
|
|
124
|
+
@logger.info("Operation completed successfully")
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
success: true,
|
|
128
|
+
result: result,
|
|
129
|
+
metadata: operation_metadata
|
|
130
|
+
}
|
|
131
|
+
rescue ValidationError => e
|
|
132
|
+
@logger.error("Validation error: #{e.message}")
|
|
133
|
+
handle_validation_error(e, operation)
|
|
134
|
+
rescue NetworkError => e
|
|
135
|
+
@logger.error("Network error: #{e.message}")
|
|
136
|
+
handle_network_error(e, operation)
|
|
137
|
+
rescue AuthorizationError => e
|
|
138
|
+
@logger.error("Authorization error: #{e.message}")
|
|
139
|
+
handle_authorization_error(e, operation)
|
|
140
|
+
rescue ResourceNotFoundError => e
|
|
141
|
+
@logger.error("Resource not found: #{e.message}")
|
|
142
|
+
handle_resource_not_found_error(e, operation)
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
@logger.error("General error: #{e.class} - #{e.message}")
|
|
145
|
+
handle_general_error(e, operation)
|
|
146
|
+
ensure
|
|
147
|
+
cleanup_resources
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# Validate operation parameters before execution
|
|
154
|
+
def validate_preconditions(operation, data, max_retries)
|
|
155
|
+
valid_operations = %w[validate process authorize]
|
|
156
|
+
unless valid_operations.include?(operation)
|
|
157
|
+
raise ValidationError.new(
|
|
158
|
+
"Invalid operation: #{operation}",
|
|
159
|
+
suggestions: ["Use one of: #{valid_operations.join(', ')}"]
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if max_retries < 0 || max_retries > 10
|
|
164
|
+
raise ValidationError.new(
|
|
165
|
+
"max_retries must be between 0 and 10, got #{max_retries}",
|
|
166
|
+
suggestions: ["Use a value between 0 and 10"]
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if operation == 'process' && data.empty?
|
|
171
|
+
raise ValidationError.new(
|
|
172
|
+
"Data is required for 'process' operation",
|
|
173
|
+
suggestions: ["Provide a data object with 'name' and 'value' fields"]
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
@logger.debug("Preconditions validated successfully")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Main operation with retry logic
|
|
181
|
+
def perform_operation(operation, data, simulate_error, max_retries)
|
|
182
|
+
retry_count = 0
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
# Simulate specific error if requested
|
|
186
|
+
simulate_error_if_requested(simulate_error, retry_count)
|
|
187
|
+
|
|
188
|
+
case operation
|
|
189
|
+
when 'validate'
|
|
190
|
+
perform_validation(data)
|
|
191
|
+
when 'process'
|
|
192
|
+
perform_processing(data)
|
|
193
|
+
when 'authorize'
|
|
194
|
+
perform_authorization(data)
|
|
195
|
+
else
|
|
196
|
+
raise ArgumentError, "Unknown operation: #{operation}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
rescue RetryableError => e
|
|
200
|
+
retry_count += 1
|
|
201
|
+
@logger.warn("Retryable error occurred, attempt #{retry_count}/#{max_retries}")
|
|
202
|
+
|
|
203
|
+
if retry_count <= max_retries
|
|
204
|
+
sleep_duration = 2 ** retry_count # Exponential backoff
|
|
205
|
+
@logger.debug("Sleeping for #{sleep_duration} seconds before retry")
|
|
206
|
+
sleep(sleep_duration)
|
|
207
|
+
retry
|
|
208
|
+
else
|
|
209
|
+
@logger.error("Max retries (#{max_retries}) exceeded")
|
|
210
|
+
raise NetworkError, "Operation failed after #{max_retries} retries: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Simulate errors for testing purposes
|
|
216
|
+
def simulate_error_if_requested(error_type, retry_count)
|
|
217
|
+
return if error_type.nil? || error_type.empty?
|
|
218
|
+
|
|
219
|
+
case error_type
|
|
220
|
+
when 'validation'
|
|
221
|
+
raise ValidationError.new(
|
|
222
|
+
"Simulated validation error",
|
|
223
|
+
suggestions: ["Fix the data format", "Check required fields"]
|
|
224
|
+
)
|
|
225
|
+
when 'network'
|
|
226
|
+
raise NetworkError, "Simulated network connection failure"
|
|
227
|
+
when 'authorization'
|
|
228
|
+
raise AuthorizationError, "Simulated authorization failure"
|
|
229
|
+
when 'retryable'
|
|
230
|
+
# Only fail on first two attempts to test retry logic
|
|
231
|
+
if retry_count < 2
|
|
232
|
+
raise RetryableError, "Simulated temporary failure (will retry)"
|
|
233
|
+
end
|
|
234
|
+
when 'resource_not_found'
|
|
235
|
+
raise ResourceNotFoundError, "Simulated resource not found"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Perform data validation
|
|
240
|
+
def perform_validation(data)
|
|
241
|
+
@logger.debug("Performing validation")
|
|
242
|
+
|
|
243
|
+
errors = []
|
|
244
|
+
warnings = []
|
|
245
|
+
|
|
246
|
+
if data[:name].nil? || data[:name].to_s.empty?
|
|
247
|
+
errors << "name is required"
|
|
248
|
+
elsif data[:name].to_s.length < 2
|
|
249
|
+
warnings << "name is very short"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if data[:value].nil?
|
|
253
|
+
errors << "value is required"
|
|
254
|
+
elsif !data[:value].is_a?(Numeric)
|
|
255
|
+
errors << "value must be a number"
|
|
256
|
+
elsif data[:value] < 0
|
|
257
|
+
warnings << "value is negative"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if errors.any?
|
|
261
|
+
raise ValidationError.new(
|
|
262
|
+
"Data validation failed: #{errors.join(', ')}",
|
|
263
|
+
suggestions: ["Ensure all required fields are present and valid"]
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
{
|
|
268
|
+
validated: true,
|
|
269
|
+
data: data,
|
|
270
|
+
warnings: warnings,
|
|
271
|
+
validated_at: Time.now.iso8601
|
|
272
|
+
}
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Perform data processing (simulates network operation)
|
|
276
|
+
def perform_processing(data)
|
|
277
|
+
@logger.debug("Performing processing")
|
|
278
|
+
|
|
279
|
+
# Simulate processing work
|
|
280
|
+
sleep(0.1)
|
|
281
|
+
|
|
282
|
+
processed_value = data[:value].to_f * 1.5
|
|
283
|
+
{
|
|
284
|
+
processed: true,
|
|
285
|
+
original_value: data[:value],
|
|
286
|
+
processed_value: processed_value.round(2),
|
|
287
|
+
name: data[:name],
|
|
288
|
+
processed_at: Time.now.iso8601
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Perform authorization check
|
|
293
|
+
def perform_authorization(data)
|
|
294
|
+
@logger.debug("Performing authorization")
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
authorized: true,
|
|
298
|
+
operation: "authorize",
|
|
299
|
+
authorized_at: Time.now.iso8601
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Validate operation results
|
|
304
|
+
def validate_postconditions(result)
|
|
305
|
+
unless result.is_a?(Hash)
|
|
306
|
+
raise StandardError, "Operation result must be a Hash"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
@logger.debug("Postconditions validated successfully")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Allocate resources (demonstration)
|
|
313
|
+
def allocate_resources
|
|
314
|
+
resource_id = SecureRandom.uuid
|
|
315
|
+
@resources_allocated << resource_id
|
|
316
|
+
@logger.debug("Allocated resource: #{resource_id}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Handle validation errors
|
|
320
|
+
def handle_validation_error(error, operation)
|
|
321
|
+
{
|
|
322
|
+
success: false,
|
|
323
|
+
error_type: "validation",
|
|
324
|
+
error: error.message,
|
|
325
|
+
suggestions: error.suggestions,
|
|
326
|
+
operation: operation,
|
|
327
|
+
support_reference: SecureRandom.uuid
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Handle network errors
|
|
332
|
+
def handle_network_error(error, operation)
|
|
333
|
+
{
|
|
334
|
+
success: false,
|
|
335
|
+
error_type: "network",
|
|
336
|
+
error: "Network operation failed: #{error.message}",
|
|
337
|
+
retry_suggested: true,
|
|
338
|
+
retry_after: 30,
|
|
339
|
+
operation: operation,
|
|
340
|
+
support_reference: SecureRandom.uuid
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Handle authorization errors
|
|
345
|
+
def handle_authorization_error(error, operation)
|
|
346
|
+
{
|
|
347
|
+
success: false,
|
|
348
|
+
error_type: "authorization",
|
|
349
|
+
error: "Access denied: #{error.message}",
|
|
350
|
+
documentation_url: "https://github.com/madbomber/shared_tools",
|
|
351
|
+
operation: operation,
|
|
352
|
+
support_reference: SecureRandom.uuid
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Handle resource not found errors
|
|
357
|
+
def handle_resource_not_found_error(error, operation)
|
|
358
|
+
{
|
|
359
|
+
success: false,
|
|
360
|
+
error_type: "resource_not_found",
|
|
361
|
+
error: "Resource not found: #{error.message}",
|
|
362
|
+
operation: operation,
|
|
363
|
+
support_reference: SecureRandom.uuid
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Handle general errors
|
|
368
|
+
def handle_general_error(error, operation)
|
|
369
|
+
{
|
|
370
|
+
success: false,
|
|
371
|
+
error_type: "general",
|
|
372
|
+
error: "An unexpected error occurred: #{error.message}",
|
|
373
|
+
error_class: error.class.name,
|
|
374
|
+
operation: operation,
|
|
375
|
+
support_reference: SecureRandom.uuid
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Collect operation metadata
|
|
380
|
+
def operation_metadata
|
|
381
|
+
execution_time = @operation_start_time ? (Time.now - @operation_start_time).round(3) : 0
|
|
382
|
+
|
|
383
|
+
{
|
|
384
|
+
execution_time_seconds: execution_time,
|
|
385
|
+
resources_allocated: @resources_allocated.length,
|
|
386
|
+
timestamp: Time.now.iso8601,
|
|
387
|
+
tool_version: "1.0.0"
|
|
388
|
+
}
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Clean up allocated resources
|
|
392
|
+
def cleanup_resources
|
|
393
|
+
if @resources_allocated.any?
|
|
394
|
+
@logger.debug("Cleaning up #{@resources_allocated.length} resources")
|
|
395
|
+
@resources_allocated.each do |resource_id|
|
|
396
|
+
@logger.debug("Released resource: #{resource_id}")
|
|
397
|
+
end
|
|
398
|
+
@resources_allocated.clear
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|