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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +594 -42
  4. data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +20 -3
  5. data/lib/shared_tools/mcp/imcp.rb +28 -0
  6. data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
  7. data/lib/shared_tools/mcp.rb +24 -0
  8. data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
  9. data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
  10. data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
  11. data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
  12. data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
  13. data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
  14. data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
  15. data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
  16. data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
  17. data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
  18. data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
  19. data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
  20. data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
  21. data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
  22. data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
  23. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
  24. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
  25. data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
  26. data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
  27. data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
  28. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
  29. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
  30. data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
  31. data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
  32. data/lib/shared_tools/tools/browser.rb +27 -0
  33. data/lib/shared_tools/tools/browser_tool.rb +255 -0
  34. data/lib/shared_tools/tools/calculator_tool.rb +169 -0
  35. data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
  36. data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
  37. data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
  38. data/lib/shared_tools/tools/computer.rb +21 -0
  39. data/lib/shared_tools/tools/computer_tool.rb +207 -0
  40. data/lib/shared_tools/tools/data_science_kit.rb +707 -0
  41. data/lib/shared_tools/tools/database/base_driver.rb +17 -0
  42. data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
  43. data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
  44. data/lib/shared_tools/tools/database.rb +9 -0
  45. data/lib/shared_tools/tools/database_query_tool.rb +313 -0
  46. data/lib/shared_tools/tools/database_tool.rb +99 -0
  47. data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
  48. data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
  49. data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
  50. data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
  51. data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
  52. data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
  53. data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
  54. data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
  55. data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
  56. data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
  57. data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
  58. data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
  59. data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
  60. data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
  61. data/lib/shared_tools/tools/disk.rb +17 -0
  62. data/lib/shared_tools/tools/disk_tool.rb +132 -0
  63. data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
  64. data/lib/shared_tools/tools/doc.rb +8 -0
  65. data/lib/shared_tools/tools/doc_tool.rb +109 -0
  66. data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
  67. data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
  68. data/lib/shared_tools/tools/docker.rb +8 -0
  69. data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
  70. data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
  71. data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
  72. data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
  73. data/lib/shared_tools/tools/eval.rb +10 -0
  74. data/lib/shared_tools/tools/eval_tool.rb +139 -0
  75. data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
  76. data/lib/shared_tools/tools/version.rb +7 -0
  77. data/lib/shared_tools/tools/weather_tool.rb +197 -0
  78. data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
  79. data/lib/shared_tools/tools.rb +16 -0
  80. data/lib/shared_tools/version.rb +1 -1
  81. data/lib/shared_tools.rb +9 -24
  82. metadata +189 -68
  83. data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
  84. data/lib/shared_tools/llm_rb.rb +0 -9
  85. data/lib/shared_tools/omniai.rb +0 -9
  86. data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
  87. data/lib/shared_tools/raix.rb +0 -9
  88. data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
  89. data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
  90. data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
  91. data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
  92. data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
  93. data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
  94. data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
  95. data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
  96. data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
  97. data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
  98. data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
  99. data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -15
  100. data/lib/shared_tools/ruby_llm/mcp.rb +0 -12
  101. data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
  102. data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
  103. data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
  104. data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
  105. data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
  106. data/lib/shared_tools/ruby_llm.rb +0 -12
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # A tool for controller a browser.
8
+ class BrowserTool < ::RubyLLM::Tool
9
+ def self.name = 'browser_tool'
10
+ module Action
11
+ VISIT = "visit"
12
+ PAGE_INSPECT = "page_inspect"
13
+ UI_INSPECT = "ui_inspect"
14
+ SELECTOR_INSPECT = "selector_inspect"
15
+ CLICK = "click"
16
+ TEXT_FIELD_SET = "text_field_set"
17
+ SCREENSHOT = "screenshot"
18
+ end
19
+
20
+ ACTIONS = [
21
+ Action::VISIT,
22
+ Action::PAGE_INSPECT,
23
+ Action::UI_INSPECT,
24
+ Action::SELECTOR_INSPECT,
25
+ Action::CLICK,
26
+ Action::TEXT_FIELD_SET,
27
+ Action::SCREENSHOT,
28
+ ].freeze
29
+
30
+ description <<~TEXT
31
+ Automates a web browser to perform various actions like visiting web pages, clicking elements, etc:
32
+
33
+ 1. `#{Action::VISIT}` - Navigate to a website
34
+ Required: "action": "visit", "url": "[website URL]"
35
+
36
+ 2. `#{Action::PAGE_INSPECT} - Get page HTML or summary
37
+ Required: "action": "page_inspect"
38
+ Optional: "full_html": true/false (get full HTML vs summary, default: summary)
39
+
40
+ 3. `#{Action::UI_INSPECT}` - Find elements by text content
41
+ Required: "action": "ui_inspect", "text_content": "[text to search for]"
42
+ Optional:
43
+ - "selector": "[CSS selector]" (search within specific container)
44
+ - "context_size": [number] (parent elements to show, default: 2)
45
+
46
+ 4. `#{Action::SELECTOR_INSPECT} - Find elements by CSS selector
47
+ Required: "action": "selector_inspect", "selector": "[CSS selector]"
48
+ Optional: "context_size": [number] (parent elements to show, default: 2)
49
+
50
+ 5. `#{Action::CLICK}` - Click an element by CSS selector
51
+ Required: "action": "click", "selector": "[CSS selector]"
52
+
53
+ 8. `#{Action::TEXT_FIELD_SET}` - Enter text in input fields/text areas
54
+ Required: "action": "text_field_set", "selector": "[field CSS selector]", "value": "[text to enter]"
55
+
56
+ 9. `#{Action::SCREENSHOT}` - Take a screenshot of the page or specific element
57
+ Required: "action": "screenshot"
58
+
59
+ ## Examples:
60
+
61
+ Visit a website
62
+ {"action": "#{Action::VISIT}", "url": "https://example.com"}
63
+ Get page summary
64
+ {"action": "#{Action::PAGE_INSPECT}"}
65
+ Get full page HTML
66
+ {"action": "#{Action::PAGE_INSPECT}", "full_html": true}
67
+ Find elements containing text
68
+ {"action": "#{Action::UI_INSPECT}", "text_content": "Submit"}
69
+ Find elements with context
70
+ {"action": "#{Action::UI_INSPECT}", "text_content": "Login", "context_size": 3}
71
+ Find elements by CSS selector
72
+ {"action": "#{Action::SELECTOR_INSPECT}", "selector": ".button"}
73
+ Find selector with context
74
+ {"action": "#{Action::SELECTOR_INSPECT}", "selector": "h1", "context_size": 2}
75
+ Click a button by CSS selector
76
+ {"action": "#{Action::CLICK}", "selector": "button[type='Submit']"}
77
+ Click a link by CSS Selector
78
+ {"action": "#{Action::CLICK}", "selector": "a[href='/login']"}
79
+ Click an element by CSS selector
80
+ {"action": "#{Action::CLICK}", "selector": "div.panel > span.toggle"}
81
+ Click an element by CSS selector:
82
+ {"action": "#{Action::CLICK}", "selector": "[role='listitem']"}
83
+ Set text in an input field
84
+ {"action": "#{Action::TEXT_FIELD_SET}", "selector": "#search", "value": "search query"}
85
+ Take a full page screenshot
86
+ {"action": "#{Action::SCREENSHOT}"}
87
+ TEXT
88
+
89
+ params do
90
+ string :action, description: <<~TEXT.strip
91
+ The browser action to perform. Options:
92
+ * `#{Action::VISIT}`: Navigate to a website
93
+ * `#{Action::PAGE_INSPECT}`: Get full HTML or summarized page information
94
+ * `#{Action::UI_INSPECT}`: Find elements containing specific text
95
+ * `#{Action::SELECTOR_INSPECT}`: Find elements matching CSS selectors
96
+ * `#{Action::CLICK}`: Click an element by CSS selector
97
+ * `#{Action::TEXT_FIELD_SET}`: Enter text in input fields or text areas
98
+ * `#{Action::SCREENSHOT}`: Take a screenshot of the current page
99
+ TEXT
100
+
101
+ string :url, description: <<~TEXT.strip, required: false
102
+ The URL to visit. Required for the following actions:
103
+ * `#{Action::VISIT}`
104
+ TEXT
105
+
106
+ string :selector, description: <<~TEXT.strip, required: false
107
+ A CSS selector to locate an element:
108
+
109
+ * 'form button[type="submit"]': selects a button with type submit
110
+ * '.example': selects elements with the foo and bar classes
111
+ * '#example': selects an element by ID
112
+ * 'div#parent > span.child': selects span elements that are direct children of div elements
113
+ * 'a[href="/login"]': selects an anchor tag with a specific href attribute
114
+ * 'button[aria-label="Close"]': selects an element by ARIA label
115
+
116
+ Required for the following actions:
117
+ * `#{Action::CLICK}`
118
+ * `#{Action::TEXT_FIELD_SET}`
119
+ * `#{Action::SELECTOR_INSPECT}`
120
+
121
+ Optional for the following actions:
122
+ * `#{Action::UI_INSPECT}` (search within specific container)
123
+ TEXT
124
+
125
+ string :value, description: <<~TEXT.strip, required: false
126
+ The value to set in the text field. Required for the following actions:
127
+ * `#{Action::TEXT_FIELD_SET}`
128
+ TEXT
129
+
130
+ integer :context_size, description: <<~TEXT.strip, required: false
131
+ Number of parent elements to include in inspect results (default: 2). Optional for the following actions:
132
+ * `#{Action::UI_INSPECT}`
133
+ * `#{Action::SELECTOR_INSPECT}`
134
+ TEXT
135
+
136
+ boolean :full_html, description: <<~TEXT.strip, required: false
137
+ Return the full HTML of the page instead of a summary. Optional for the following actions:
138
+ * `#{Action::PAGE_INSPECT}`
139
+ TEXT
140
+
141
+ string :text_content, description: <<~TEXT.strip, required: false
142
+ Search for elements containing this text. Required for the following actions:
143
+ * `#{Action::UI_INSPECT}`
144
+ TEXT
145
+ end
146
+
147
+
148
+ # @param logger [Logger] optional logger
149
+ # @param driver [SharedTools::Tools::Browser::BaseDriver] optional, will attempt to create WatirDriver if not provided
150
+ def initialize(logger: nil, driver: nil)
151
+ @logger = logger || RubyLLM.logger
152
+ @driver = driver || default_driver
153
+ end
154
+
155
+ def cleanup!
156
+ @driver.close
157
+ end
158
+
159
+ # @param action [String]
160
+ # @param url [String, nil]
161
+ # @param selector [String, nil] e.g. "button[type='submit']", "div#parent > span.child", etc
162
+ # @param value [String, nil]
163
+ # @param context_size [Integer]
164
+ # @param full_html [Boolean]
165
+ # @param text_content [String, nil]
166
+ #
167
+ # @return [String]
168
+ def execute(action:, url: nil, selector: nil, value: nil, context_size: 2, full_html: false, text_content: nil)
169
+ case action.to_s.downcase
170
+ when Action::VISIT
171
+ require_param!(:url, url)
172
+ visit_tool.execute(url:)
173
+ when Action::PAGE_INSPECT
174
+ if full_html
175
+ page_inspect_tool.execute
176
+ else
177
+ page_inspect_tool.execute(summarize: true)
178
+ end
179
+ when Action::UI_INSPECT
180
+ require_param!(:text_content, text_content)
181
+ inspect_tool.execute(text_content:, selector:, context_size:)
182
+ when Action::SELECTOR_INSPECT
183
+ require_param!(:selector, selector)
184
+ selector_inspect_tool.execute(selector:, context_size:)
185
+ when Action::CLICK
186
+ require_param!(:selector, selector)
187
+ click_tool.execute(selector:)
188
+ when Action::TEXT_FIELD_SET
189
+ require_param!(:selector, selector)
190
+ require_param!(:value, value)
191
+ text_field_area_set_tool.execute(selector:, text: value)
192
+ when Action::SCREENSHOT
193
+ page_screenshot_tool.execute
194
+ else
195
+ { error: "Unsupported action: #{action}. Supported actions are: #{ACTIONS.join(', ')}" }
196
+ end
197
+ end
198
+
199
+ private
200
+
201
+ # @param name [Symbol]
202
+ # @param value [Object]
203
+ #
204
+ # @raise [ArgumentError]
205
+ # @return [void]
206
+ def require_param!(name, value)
207
+ raise ArgumentError, "#{name} param is required for this action" if value.nil?
208
+ end
209
+
210
+ # @return [Browser::VisitTool]
211
+ def visit_tool
212
+ Browser::VisitTool.new(driver: @driver, logger: @logger)
213
+ end
214
+
215
+ # @return [Browser::PageInspectTool]
216
+ def page_inspect_tool
217
+ Browser::PageInspectTool.new(driver: @driver, logger: @logger)
218
+ end
219
+
220
+ # @return [Browser::UIInspectTool]
221
+ def inspect_tool
222
+ Browser::InspectTool.new(driver: @driver, logger: @logger)
223
+ end
224
+
225
+ # @return [Browser::SelectorInspectTool]
226
+ def selector_inspect_tool
227
+ Browser::SelectorInspectTool.new(driver: @driver, logger: @logger)
228
+ end
229
+
230
+ # @return [Browser::ClickTool]
231
+ def click_tool
232
+ Browser::ClickTool.new(driver: @driver, logger: @logger)
233
+ end
234
+
235
+ # @return [Browser::TextFieldAreaSetTool]
236
+ def text_field_area_set_tool
237
+ Browser::TextFieldAreaSetTool.new(driver: @driver, logger: @logger)
238
+ end
239
+
240
+ # @return [Browser::PageScreenshotTool]
241
+ def page_screenshot_tool
242
+ Browser::PageScreenshotTool.new(driver: @driver, logger: @logger)
243
+ end
244
+
245
+ # @return [Browser::BaseDriver]
246
+ def default_driver
247
+ if defined?(Watir)
248
+ Browser::WatirDriver.new(logger: @logger)
249
+ else
250
+ raise LoadError, "BrowserTool requires a driver. Either install the 'watir' gem or pass a driver: parameter"
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,169 @@
1
+ # calculator_tool.rb - Safe mathematical calculator
2
+ require 'ruby_llm/tool'
3
+ require 'dentaku'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ class CalculatorTool < RubyLLM::Tool
8
+ def self.name = 'calculator'
9
+
10
+ description <<~'DESCRIPTION'
11
+ Perform advanced mathematical calculations with comprehensive error handling and validation.
12
+ This tool supports basic arithmetic operations, parentheses, and common mathematical functions.
13
+ It uses Dentaku for safe evaluation of mathematical expressions without executing arbitrary code,
14
+ making it suitable for use in AI-assisted calculations where security is critical.
15
+ The tool returns formatted results with configurable precision and helpful error messages
16
+ when invalid expressions are provided.
17
+
18
+ Supported operations:
19
+ - Basic arithmetic: +, -, *, /, %
20
+ - Parentheses for grouping: ( )
21
+ - Exponentiation: ^ or pow(base, exponent)
22
+ - Comparison operators: =, <, >, <=, >=, !=
23
+ - Logical operators: and, or, not
24
+ - Mathematical functions: sqrt, round, roundup, rounddown, abs
25
+ - Trigonometric functions: sin, cos, tan
26
+
27
+ Example usage:
28
+ tool = SharedTools::Tools::CalculatorTool.new
29
+ result = tool.execute(expression: "2 + 2")
30
+ puts "Result: #{result[:result]}"
31
+
32
+ # With precision
33
+ result = tool.execute(expression: "10 / 3", precision: 4)
34
+ puts "Result: #{result[:result]}" # 3.3333
35
+ DESCRIPTION
36
+
37
+ params do
38
+ string :expression, description: <<~DESC.strip
39
+ Mathematical expression to evaluate using standard arithmetic operators and parentheses.
40
+ Supported operations include: addition (+), subtraction (-), multiplication (*),
41
+ division (/), modulo (%), exponentiation (**), and parentheses for grouping.
42
+ Examples: '2 + 2', '(10 * 5) / 2', '15.5 - 3.2', 'sqrt(16)', 'round(3.14159, 2)'.
43
+ The expression is evaluated safely using Dentaku math parser.
44
+ DESC
45
+
46
+ integer :precision, description: <<~DESC.strip, required: false
47
+ Number of decimal places to display in the result. Must be between 0 and 10.
48
+ Set to 0 for whole numbers only, or higher values for more precise decimal results.
49
+ Default is 2 decimal places, which works well for most financial and general calculations.
50
+ DESC
51
+ end
52
+
53
+ # @param logger [Logger] optional logger
54
+ def initialize(logger: nil)
55
+ @logger = logger || RubyLLM.logger
56
+ @calculator = Dentaku::Calculator.new
57
+
58
+ # Enable case-insensitive function names
59
+ Dentaku.enable_caching!
60
+ end
61
+
62
+ # Execute mathematical calculation
63
+ #
64
+ # @param expression [String] Mathematical expression to evaluate
65
+ # @param precision [Integer] Number of decimal places (0-10), default 2
66
+ #
67
+ # @return [Hash] Calculation result with success status
68
+ def execute(expression:, precision: 2)
69
+ @logger.info("CalculatorTool#execute expression=#{expression.inspect} precision=#{precision}")
70
+
71
+ begin
72
+ # Validate precision
73
+ precision = validate_precision(precision)
74
+
75
+ # Evaluate expression safely using Dentaku
76
+ result = @calculator.evaluate(expression)
77
+
78
+ # Handle nil result (invalid expression)
79
+ unless result
80
+ raise Dentaku::ParseError, "Could not parse expression"
81
+ end
82
+
83
+ # Format result with specified precision
84
+ formatted_result = format_result(result, precision)
85
+
86
+ @logger.info("Calculation successful: #{expression} = #{formatted_result}")
87
+
88
+ {
89
+ success: true,
90
+ result: formatted_result,
91
+ expression: expression,
92
+ precision: precision,
93
+ raw_result: result
94
+ }
95
+ rescue Dentaku::ParseError => e
96
+ @logger.error("Parse error for expression '#{expression}': #{e.message}")
97
+ {
98
+ success: false,
99
+ error: "Invalid expression: #{e.message}",
100
+ expression: expression,
101
+ suggestion: "Try expressions like '2 + 2', '(10 * 5) / 2', or 'sqrt(16)'"
102
+ }
103
+ rescue Dentaku::ArgumentError => e
104
+ @logger.error("Argument error for expression '#{expression}': #{e.message}")
105
+ {
106
+ success: false,
107
+ error: "Invalid arguments: #{e.message}",
108
+ expression: expression,
109
+ suggestion: "Check that functions have the correct number of arguments"
110
+ }
111
+ rescue ZeroDivisionError => e
112
+ @logger.error("Division by zero in expression '#{expression}'")
113
+ {
114
+ success: false,
115
+ error: "Division by zero",
116
+ expression: expression,
117
+ suggestion: "Ensure the denominator is not zero"
118
+ }
119
+ rescue => e
120
+ @logger.error("Calculation failed for '#{expression}': #{e.message}")
121
+ {
122
+ success: false,
123
+ error: "Calculation error: #{e.message}",
124
+ expression: expression,
125
+ suggestion: "Verify the expression syntax and try again"
126
+ }
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ # Validate and normalize precision value
133
+ #
134
+ # @param precision [Integer] Requested precision
135
+ # @return [Integer] Validated precision (0-10)
136
+ def validate_precision(precision)
137
+ precision = precision.to_i
138
+
139
+ if precision < 0
140
+ @logger.warn("Negative precision #{precision} adjusted to 0")
141
+ return 0
142
+ end
143
+
144
+ if precision > 10
145
+ @logger.warn("Precision #{precision} exceeds maximum, adjusted to 10")
146
+ return 10
147
+ end
148
+
149
+ precision
150
+ end
151
+
152
+ # Format result with specified precision
153
+ #
154
+ # @param result [Numeric] Raw calculation result
155
+ # @param precision [Integer] Number of decimal places
156
+ # @return [Numeric] Formatted result
157
+ def format_result(result, precision)
158
+ # Handle boolean results
159
+ return result if result == true || result == false
160
+
161
+ # Handle numeric results
162
+ return result unless result.is_a?(Numeric)
163
+
164
+ # Round to specified precision
165
+ result.round(precision)
166
+ end
167
+ end
168
+ end
169
+ end