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,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
module Browser
|
|
8
|
+
# A browser automation tool for taking screenshots of the current page.
|
|
9
|
+
class PageScreenshotTool < ::RubyLLM::Tool
|
|
10
|
+
def self.name = 'browser_page_screenshot'
|
|
11
|
+
|
|
12
|
+
description "A browser automation tool for taking screenshots of the current page."
|
|
13
|
+
|
|
14
|
+
def initialize(driver: nil, logger: nil)
|
|
15
|
+
@driver = driver || default_driver
|
|
16
|
+
@logger = logger || RubyLLM.logger
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute
|
|
20
|
+
@logger.info("#{self.class.name}##{__method__}")
|
|
21
|
+
|
|
22
|
+
@driver.screenshot do |file|
|
|
23
|
+
"data:image/png;base64,#{Base64.strict_encode64(file.read)}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def default_driver
|
|
30
|
+
if defined?(Watir)
|
|
31
|
+
WatirDriver.new(logger: @logger)
|
|
32
|
+
else
|
|
33
|
+
raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
module SelectorGenerator
|
|
7
|
+
# Basic selector generation methods
|
|
8
|
+
module BaseSelectors
|
|
9
|
+
def placeholder_selector(element, tag)
|
|
10
|
+
valid_attribute?(element["placeholder"]) ? ["#{tag}[placeholder=\"#{element['placeholder']}\"]"] : []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def aria_label_selector(element, tag)
|
|
14
|
+
valid_attribute?(element["aria-label"]) ? ["#{tag}[aria-label=\"#{element['aria-label']}\"]"] : []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def name_selector(element, tag)
|
|
18
|
+
valid_attribute?(element["name"]) ? ["#{tag}[name=\"#{element['name']}\"]"] : []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def maxlength_selector(element, tag)
|
|
22
|
+
valid_attribute?(element["maxlength"]) ? ["#{tag}[maxlength=\"#{element['maxlength']}\"]"] : []
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
module SelectorGenerator
|
|
7
|
+
# Context-aware selector generation for complex elements
|
|
8
|
+
module ContextualSelectors
|
|
9
|
+
def generate_contextual_selectors(element)
|
|
10
|
+
selectors = []
|
|
11
|
+
selectors.concat(parent_class_selectors(element))
|
|
12
|
+
selectors.concat(label_based_selectors(element))
|
|
13
|
+
selectors.concat(position_based_selectors(element))
|
|
14
|
+
selectors
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate selectors based on parent container classes
|
|
18
|
+
def parent_class_selectors(element)
|
|
19
|
+
significant_parent = find_significant_parent(element)
|
|
20
|
+
return [] unless significant_parent
|
|
21
|
+
|
|
22
|
+
parent_class = most_specific_class(significant_parent)
|
|
23
|
+
return [] unless parent_class
|
|
24
|
+
|
|
25
|
+
build_parent_selector(element, parent_class)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Build selector with parent class context
|
|
29
|
+
def build_parent_selector(element, parent_class)
|
|
30
|
+
base = ".#{parent_class} #{element.name}"
|
|
31
|
+
return ["#{base}[placeholder=\"#{element['placeholder']}\"]"] if element["placeholder"]
|
|
32
|
+
return ["#{base}[type=\"#{element['type']}\"]"] if element["type"]
|
|
33
|
+
|
|
34
|
+
[base]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Find parent with meaningful class (not generic like 'row' or 'col')
|
|
38
|
+
def find_significant_parent(element)
|
|
39
|
+
parent = element.parent
|
|
40
|
+
while parent && parent.name != "body"
|
|
41
|
+
return parent if element_has_significant_class?(parent)
|
|
42
|
+
|
|
43
|
+
parent = parent.parent
|
|
44
|
+
end
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if element has significant class
|
|
49
|
+
def element_has_significant_class?(element)
|
|
50
|
+
classes = element["class"]&.split || []
|
|
51
|
+
classes.any? { |c| significant_class?(c) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if class name is likely to be meaningful/specific
|
|
55
|
+
def significant_class?(class_name)
|
|
56
|
+
return false if class_name.length < 4
|
|
57
|
+
return false if generic_class?(class_name)
|
|
58
|
+
|
|
59
|
+
class_name.match?(/[a-z]+[-_]?[a-z]+/i)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Common generic class names to ignore
|
|
63
|
+
def generic_class?(class_name)
|
|
64
|
+
%w[row col container wrapper inner outer main].include?(class_name.downcase)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get most specific (longest) class name
|
|
68
|
+
def most_specific_class(element)
|
|
69
|
+
classes = element["class"]&.split || []
|
|
70
|
+
classes.select { |c| significant_class?(c) }.max_by(&:length)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Generate selectors based on label associations
|
|
74
|
+
def label_based_selectors(element)
|
|
75
|
+
return [] unless stable_id?(element)
|
|
76
|
+
|
|
77
|
+
label = find_label_for_element(element)
|
|
78
|
+
label ? ["#{element.name}##{element['id']}"] : []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if element has stable (non-React) ID
|
|
82
|
+
def stable_id?(element)
|
|
83
|
+
id = element["id"]
|
|
84
|
+
id && !id.empty? && !id.match?(/^:r[0-9a-z]+:$/i)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Find label element associated with this element
|
|
88
|
+
def find_label_for_element(element)
|
|
89
|
+
element.document.at_css("label[for=\"#{element['id']}\"]")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate position-based selectors for similar elements
|
|
93
|
+
def position_based_selectors(element)
|
|
94
|
+
siblings = find_similar_siblings(element)
|
|
95
|
+
return [] unless siblings.size > 1
|
|
96
|
+
|
|
97
|
+
index = siblings.index(element) + 1
|
|
98
|
+
parent_context = parent_context_prefix(element)
|
|
99
|
+
build_position_selector(element, index, parent_context)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Build nth-of-type selector
|
|
103
|
+
def build_position_selector(element, index, parent_context = "")
|
|
104
|
+
nth = ":nth-of-type(#{index})"
|
|
105
|
+
base = "#{parent_context}#{element.name}#{nth}"
|
|
106
|
+
return ["#{parent_context}#{element.name}[type=\"#{element['type']}\"]#{nth}"] if element["type"]
|
|
107
|
+
if element["placeholder"]
|
|
108
|
+
return ["#{parent_context}#{element.name}[placeholder=\"#{element['placeholder']}\"]#{nth}"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
[base]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Find sibling elements of same type with similar attributes
|
|
115
|
+
def find_similar_siblings(element)
|
|
116
|
+
return [] unless element.parent
|
|
117
|
+
|
|
118
|
+
element.parent.css(element.name).select { |sibling| same_key_attributes?(element, sibling) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if two elements have same key attributes
|
|
122
|
+
def same_key_attributes?(elem1, elem2)
|
|
123
|
+
return false unless elem1.name == elem2.name
|
|
124
|
+
|
|
125
|
+
elem1.name == "input" ? elem1["type"] == elem2["type"] : true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Get parent context for more specific position selectors
|
|
129
|
+
def parent_context_prefix(element)
|
|
130
|
+
parent = find_significant_parent(element)
|
|
131
|
+
return "" unless parent
|
|
132
|
+
|
|
133
|
+
parent_class = most_specific_class(parent)
|
|
134
|
+
parent_class ? ".#{parent_class} " : ""
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
# Generates stable CSS selectors for HTML elements
|
|
7
|
+
module SelectorGenerator
|
|
8
|
+
extend BaseSelectors
|
|
9
|
+
extend ContextualSelectors
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def generate_stable_selectors(element)
|
|
14
|
+
return [] unless valid_element?(element)
|
|
15
|
+
|
|
16
|
+
selectors = []
|
|
17
|
+
selectors.concat(generate_by_type(element))
|
|
18
|
+
selectors.concat(generate_contextual_selectors(element))
|
|
19
|
+
selectors.compact.uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_by_type(element)
|
|
23
|
+
case element.name
|
|
24
|
+
when "input" then generate_input_selectors(element)
|
|
25
|
+
when "textarea" then generate_textarea_selectors(element)
|
|
26
|
+
when "select" then generate_select_selectors(element)
|
|
27
|
+
else []
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid_element?(element)
|
|
32
|
+
element.respond_to?(:name) && element.respond_to?(:parent)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generate_input_selectors(element)
|
|
36
|
+
selectors = []
|
|
37
|
+
selectors.concat(placeholder_selector(element, "input"))
|
|
38
|
+
selectors.concat(aria_label_selector(element, "input"))
|
|
39
|
+
selectors.concat(type_selectors(element))
|
|
40
|
+
selectors.concat(attribute_selectors(element, "input"))
|
|
41
|
+
selectors
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def generate_textarea_selectors(element)
|
|
45
|
+
placeholder_selector(element, "textarea") + name_selector(element, "textarea")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def generate_select_selectors(element)
|
|
49
|
+
name_selector(element, "select") + aria_label_selector(element, "select")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def type_selectors(element)
|
|
53
|
+
return [] unless valid_attribute?(element["type"])
|
|
54
|
+
|
|
55
|
+
base = "input[type=\"#{element['type']}\"]"
|
|
56
|
+
[base, amount_class_selector(base, element)].compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def amount_class_selector(base, element)
|
|
60
|
+
element["class"]&.include?("wv-input--amount") ? "#{base}.wv-input--amount" : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def attribute_selectors(element, tag)
|
|
64
|
+
maxlength_selector(element, tag) + name_selector(element, tag)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def valid_attribute?(attribute)
|
|
68
|
+
attribute && attribute.strip.length.positive?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Nokogiri is optional - will raise error when tool is used without it
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module SharedTools
|
|
10
|
+
module Tools
|
|
11
|
+
module Browser
|
|
12
|
+
# A browser automation tool for inspecting elements using CSS selectors.
|
|
13
|
+
class SelectorInspectTool < ::RubyLLM::Tool
|
|
14
|
+
def self.name = 'browser_selector_inspect'
|
|
15
|
+
|
|
16
|
+
include InspectUtils
|
|
17
|
+
|
|
18
|
+
description "A browser automation tool for finding and inspecting elements by CSS selector."
|
|
19
|
+
|
|
20
|
+
params do
|
|
21
|
+
string :selector, description: "CSS selector to target specific elements"
|
|
22
|
+
integer :context_size, description: "Number of parent elements to include for context", required: false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(driver: nil, logger: nil)
|
|
26
|
+
@driver = driver || default_driver
|
|
27
|
+
@logger = logger || RubyLLM.logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute(selector:, context_size: 2)
|
|
31
|
+
raise LoadError, "SelectorInspectTool requires the 'nokogiri' gem. Install it with: gem install nokogiri" unless defined?(Nokogiri)
|
|
32
|
+
|
|
33
|
+
@logger.info("#{self.class.name}##{__method__}")
|
|
34
|
+
|
|
35
|
+
doc = cleaned_document(html: @driver.html)
|
|
36
|
+
target_elements = doc.css(selector)
|
|
37
|
+
|
|
38
|
+
return "No elements found matching selector: #{selector}" if target_elements.empty?
|
|
39
|
+
|
|
40
|
+
format_elements(target_elements, selector, context_size)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def default_driver
|
|
46
|
+
if defined?(Watir)
|
|
47
|
+
WatirDriver.new(logger: @logger)
|
|
48
|
+
else
|
|
49
|
+
raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def format_elements(elements, selector, context_size)
|
|
54
|
+
result = "Found #{elements.size} elements matching '#{selector}':\n\n"
|
|
55
|
+
|
|
56
|
+
elements.each_with_index do |element, index|
|
|
57
|
+
result += "--- Element #{index + 1} ---\n"
|
|
58
|
+
result += Formatters::ElementFormatter.get_parent_context(element, context_size) if context_size.positive?
|
|
59
|
+
result += "Element: #{element.to_html}\n\n"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
# @example
|
|
7
|
+
# browser = Watir::Browser.new(:chrome)
|
|
8
|
+
# tool = SharedTools::Tools::Browser::TextFieldSetTool.new(browser:)
|
|
9
|
+
# tool.execute(selector: "...", text: "...")
|
|
10
|
+
class TextFieldAreaSetTool < ::RubyLLM::Tool
|
|
11
|
+
def self.name = 'browser_text_field_set'
|
|
12
|
+
|
|
13
|
+
description "A browser automation tool for clicking a specific link."
|
|
14
|
+
|
|
15
|
+
params do
|
|
16
|
+
string :selector, description: "The ID / name of the text field / area to interact with."
|
|
17
|
+
string :text, description: "The text to set."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(driver: nil, logger: nil)
|
|
21
|
+
@driver = driver || default_driver
|
|
22
|
+
@logger = logger || RubyLLM.logger
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param selector [String] The ID / name of the text field / text area to interact with.
|
|
26
|
+
# @param text [String] The text to set.
|
|
27
|
+
def execute(selector:, text:)
|
|
28
|
+
@logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
|
|
29
|
+
|
|
30
|
+
@driver.fill_in(selector:, text:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def default_driver
|
|
36
|
+
if defined?(Watir)
|
|
37
|
+
WatirDriver.new(logger: @logger)
|
|
38
|
+
else
|
|
39
|
+
raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
# @example
|
|
7
|
+
# driver = Selenium::WebDriver.for :chrome
|
|
8
|
+
# tool = SharedTools::Tools::Browser::VisitTool.new(driver:)
|
|
9
|
+
# tool.execute(to: "https://news.ycombinator.com")
|
|
10
|
+
class VisitTool < ::RubyLLM::Tool
|
|
11
|
+
def self.name = 'browser_visit'
|
|
12
|
+
|
|
13
|
+
description "A browser automation tool for navigating to a specific URL."
|
|
14
|
+
|
|
15
|
+
params do
|
|
16
|
+
string :url, description: "A URL (e.g. https://news.ycombinator.com)."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(driver: nil, logger: nil)
|
|
20
|
+
@driver = driver || default_driver
|
|
21
|
+
@logger = logger || RubyLLM.logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param url [String] A URL (e.g. https://news.ycombinator.com).
|
|
25
|
+
def execute(url:)
|
|
26
|
+
@logger.info("#{self.class.name}##{__method__} url=#{url.inspect}")
|
|
27
|
+
|
|
28
|
+
@driver.goto(url:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def default_driver
|
|
34
|
+
if defined?(Watir)
|
|
35
|
+
WatirDriver.new(logger: @logger)
|
|
36
|
+
else
|
|
37
|
+
raise LoadError, "Browser tools require a driver. Either install the 'watir' gem or pass a driver: parameter"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Browser
|
|
6
|
+
# @example
|
|
7
|
+
# driver = SharedTools::Tools::Browser::WatirDriver.new
|
|
8
|
+
# driver.visit("https://example.com")
|
|
9
|
+
# driver.click(id: "submit-button")
|
|
10
|
+
class WatirDriver < BaseDriver
|
|
11
|
+
def initialize(logger: Logger.new(IO::NULL), browser: Watir::Browser.new(:chrome))
|
|
12
|
+
super(logger:)
|
|
13
|
+
@browser = browser
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def close
|
|
17
|
+
@browser.close
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
def url
|
|
22
|
+
@browser.url
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [String]
|
|
26
|
+
def title
|
|
27
|
+
@browser.title
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String]
|
|
31
|
+
def html
|
|
32
|
+
@browser.html
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param url [String]
|
|
36
|
+
def goto(url:)
|
|
37
|
+
@browser.goto(url)
|
|
38
|
+
|
|
39
|
+
{ status: :ok }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @yield [file]
|
|
43
|
+
# @yieldparam file [File]
|
|
44
|
+
def screenshot
|
|
45
|
+
tempfile = Tempfile.new(["screenshot", ".png"])
|
|
46
|
+
@browser.screenshot.save(tempfile.path)
|
|
47
|
+
|
|
48
|
+
yield File.open(tempfile.path, "rb")
|
|
49
|
+
ensure
|
|
50
|
+
tempfile&.close
|
|
51
|
+
tempfile&.unlink
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param selector [String]
|
|
55
|
+
# @param text [String]
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash]
|
|
58
|
+
def fill_in(selector:, text:)
|
|
59
|
+
element = find_field(selector)
|
|
60
|
+
|
|
61
|
+
return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
|
|
62
|
+
|
|
63
|
+
element.set(text)
|
|
64
|
+
|
|
65
|
+
{ status: :ok }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param selector [String] e.g. "button[type='submit']", "div#parent > span.child", etc
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
def click(selector:)
|
|
72
|
+
element = find_element(selector)
|
|
73
|
+
|
|
74
|
+
return { status: :error, message: "unknown selector=#{selector.inspect}" } if element.nil?
|
|
75
|
+
|
|
76
|
+
element.click
|
|
77
|
+
|
|
78
|
+
{ status: :ok }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
protected
|
|
82
|
+
|
|
83
|
+
def wait_for_element(&)
|
|
84
|
+
Watir::Wait.until(timeout: TIMEOUT, &)
|
|
85
|
+
rescue Watir::Wait::TimeoutError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param selector [String]
|
|
90
|
+
#
|
|
91
|
+
# @return [Watir::Input, Watir::TextArea, nil]
|
|
92
|
+
def find_field(selector)
|
|
93
|
+
wait_for_element { find_input_by(css: selector) || find_textarea_by(css: selector) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param selector [Hash] A hash with one of the following
|
|
97
|
+
#
|
|
98
|
+
# @return [Watir::Element, nil]
|
|
99
|
+
def find_element(selector)
|
|
100
|
+
wait_for_element { find_element_by(css: selector) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_element_by(selector)
|
|
104
|
+
element = @browser.element(selector)
|
|
105
|
+
return unless element.respond_to?(:exists?)
|
|
106
|
+
|
|
107
|
+
element if element.exists?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param selector [Hash]
|
|
111
|
+
#
|
|
112
|
+
# @return [Watir::TextArea, nil]
|
|
113
|
+
def find_textarea_by(selector)
|
|
114
|
+
element = @browser.textarea(selector)
|
|
115
|
+
return unless element
|
|
116
|
+
|
|
117
|
+
element if element.exists?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param selector [Hash]
|
|
121
|
+
#
|
|
122
|
+
# @return [Watir::Input, nil]
|
|
123
|
+
def find_input_by(selector)
|
|
124
|
+
element = @browser.input(selector)
|
|
125
|
+
return unless element
|
|
126
|
+
|
|
127
|
+
element if element.exists?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collection loader for all browser tools
|
|
4
|
+
# Usage: require 'shared_tools/tools/browser'
|
|
5
|
+
|
|
6
|
+
require 'shared_tools'
|
|
7
|
+
|
|
8
|
+
# Load base classes and utilities first (required by other components)
|
|
9
|
+
require_relative 'browser/base_driver'
|
|
10
|
+
require_relative 'browser/inspect_utils'
|
|
11
|
+
|
|
12
|
+
# Try to load watir for browser automation
|
|
13
|
+
begin
|
|
14
|
+
require 'watir'
|
|
15
|
+
require_relative 'browser/watir_driver'
|
|
16
|
+
rescue LoadError
|
|
17
|
+
# Watir gem not installed, BrowserTools will require manual driver
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Load tools (order matters - utils loaded first)
|
|
21
|
+
require_relative 'browser/visit_tool'
|
|
22
|
+
require_relative 'browser/click_tool'
|
|
23
|
+
require_relative 'browser/inspect_tool'
|
|
24
|
+
require_relative 'browser/page_inspect_tool'
|
|
25
|
+
require_relative 'browser/page_screenshot_tool'
|
|
26
|
+
require_relative 'browser/selector_inspect_tool'
|
|
27
|
+
require_relative 'browser/text_field_area_set_tool'
|