shared_tools 0.2.1 → 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 +31 -24
- 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 -33
- 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 -33
- data/lib/shared_tools/ruby_llm/mcp.rb +0 -10
- 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,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
|