omniai-tools 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -12
- data/lib/omniai/tools/browser/base_driver.rb +78 -0
- data/lib/omniai/tools/browser/base_tool.rb +31 -4
- data/lib/omniai/tools/browser/button_click_tool.rb +1 -14
- data/lib/omniai/tools/browser/element_click_tool.rb +30 -0
- data/lib/omniai/tools/browser/elements/element_grouper.rb +73 -0
- data/lib/omniai/tools/browser/elements/nearby_element_detector.rb +108 -0
- data/lib/omniai/tools/browser/formatters/action_formatter.rb +37 -0
- data/lib/omniai/tools/browser/formatters/data_entry_formatter.rb +135 -0
- data/lib/omniai/tools/browser/formatters/element_formatter.rb +52 -0
- data/lib/omniai/tools/browser/formatters/input_formatter.rb +59 -0
- data/lib/omniai/tools/browser/inspect_tool.rb +46 -13
- data/lib/omniai/tools/browser/inspect_utils.rb +51 -0
- data/lib/omniai/tools/browser/link_click_tool.rb +2 -14
- data/lib/omniai/tools/browser/page_inspect/button_summarizer.rb +140 -0
- data/lib/omniai/tools/browser/page_inspect/form_summarizer.rb +98 -0
- data/lib/omniai/tools/browser/page_inspect/html_summarizer.rb +37 -0
- data/lib/omniai/tools/browser/page_inspect/link_summarizer.rb +103 -0
- data/lib/omniai/tools/browser/page_inspect_tool.rb +30 -0
- data/lib/omniai/tools/browser/page_screenshot_tool.rb +22 -0
- data/lib/omniai/tools/browser/selector_generator/base_selectors.rb +28 -0
- data/lib/omniai/tools/browser/selector_generator/contextual_selectors.rb +140 -0
- data/lib/omniai/tools/browser/selector_generator.rb +73 -0
- data/lib/omniai/tools/browser/selector_inspect_tool.rb +44 -0
- data/lib/omniai/tools/browser/text_field_area_set_tool.rb +2 -31
- data/lib/omniai/tools/browser/visit_tool.rb +1 -1
- data/lib/omniai/tools/browser/watir_driver.rb +224 -0
- data/lib/omniai/tools/browser_tool.rb +265 -0
- data/lib/omniai/tools/version.rb +1 -1
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc814f27a50bb0ecceaff25eb4153aed6ade80ca24f7abcb74198e0a31afe627
|
4
|
+
data.tar.gz: c68820e0b080dc4812cd15871c8da74963d27b20960dd9e9e254b214bb313dfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ad4b2eb6e1cee4bbfd6b34f1957bb663de22785f4b2071de5323d38d5614c9438f813bbab9b98b9a232335a43243e82191103e59aca054400323f6137f5886e
|
7
|
+
data.tar.gz: faf87bbacaf66022c34beb835674deda75b359e937bc4a734918b8f2cc28985438eba994b40ac0553c8fb1ab7bfa433532cd4cbe0d6dffa3fa5298fa904565dd
|
data/README.md
CHANGED
@@ -16,20 +16,11 @@ Database tools are focused on running SQL statements:
|
|
16
16
|
require "omniai/openai"
|
17
17
|
require "omniai/tools"
|
18
18
|
|
19
|
-
require "watir"
|
20
|
-
|
21
|
-
browser = Watir::Browser.new(:chrome)
|
22
|
-
|
23
19
|
client = OmniAI::OpenAI::Client.new
|
24
|
-
logger = Logger.new($stdout)
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
OmniAI::Tools::Browser::ButtonClickTool,
|
30
|
-
OmniAI::Tools::Browser::LinkClickTool,
|
31
|
-
OmniAI::Tools::Browser::TextFieldAreaSetTool,
|
32
|
-
].map { |klass| klass.new(browser:, logger:) }
|
21
|
+
logger = Logger.new($stdout)
|
22
|
+
driver = OmniAI::Tools::Browser::WatirDriver.new
|
23
|
+
tools = [OmniAI::Tools::BrowserTool.new(driver: logger:)]
|
33
24
|
|
34
25
|
puts "Type 'exit' or 'quit' to leave."
|
35
26
|
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
# A base driver intended to be overridden by specific browser drivers (e.g. waitir).
|
7
|
+
class BaseDriver
|
8
|
+
TIMEOUT = Integer(ENV.fetch("OMNIAI_BROWSER_TIMEOUT", 10))
|
9
|
+
|
10
|
+
# @param logger [Logger]
|
11
|
+
def initialize(logger: Logger.new(IO::NULL))
|
12
|
+
@logger = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def url
|
21
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String]
|
25
|
+
def title
|
26
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
def html
|
31
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
32
|
+
end
|
33
|
+
|
34
|
+
# @yield [file]
|
35
|
+
# @yieldparam file [File]
|
36
|
+
def screenshot
|
37
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param url [String]
|
41
|
+
#
|
42
|
+
# @return [Hash]
|
43
|
+
def goto(url:)
|
44
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param selector [String]
|
48
|
+
# @param text [String]
|
49
|
+
#
|
50
|
+
# @return [Hash]
|
51
|
+
def fill_in(selector:, text:)
|
52
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param selector [String]
|
56
|
+
#
|
57
|
+
# @return [Hash]
|
58
|
+
def button_click(selector:)
|
59
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param selector [String]
|
63
|
+
#
|
64
|
+
# @return [Hash]
|
65
|
+
def link_click(selector:)
|
66
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param selector [String]
|
70
|
+
#
|
71
|
+
# @return [Hash]
|
72
|
+
def element_click(selector:)
|
73
|
+
raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -10,12 +10,39 @@ module OmniAI
|
|
10
10
|
# # ...
|
11
11
|
# end
|
12
12
|
class BaseTool < OmniAI::Tool
|
13
|
-
# @param logger [
|
14
|
-
# @param
|
15
|
-
def initialize(
|
13
|
+
# @param logger [Logger]
|
14
|
+
# @param driver [BaseDriver]
|
15
|
+
def initialize(driver:, logger: Logger.new(IO::NULL))
|
16
16
|
super()
|
17
|
+
@driver = driver
|
17
18
|
@logger = logger
|
18
|
-
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def wait_for_element
|
24
|
+
return yield if defined?(RSpec) # Skip waiting in tests
|
25
|
+
|
26
|
+
Watir::Wait.until(timeout: 10) do
|
27
|
+
element = yield
|
28
|
+
element if element && element_visible?(element)
|
29
|
+
end
|
30
|
+
rescue Watir::Wait::TimeoutError
|
31
|
+
log_element_timeout
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def element_visible?(element)
|
36
|
+
return true unless element.respond_to?(:visible?)
|
37
|
+
|
38
|
+
element.visible?
|
39
|
+
end
|
40
|
+
|
41
|
+
def log_element_timeout
|
42
|
+
return unless @browser.respond_to?(:elements)
|
43
|
+
|
44
|
+
visible_elements = @browser.elements.select(&:visible?).map(&:text).compact.first(10)
|
45
|
+
@logger.error("Element not found after 10s. Sample visible elements: #{visible_elements}")
|
19
46
|
end
|
20
47
|
end
|
21
48
|
end
|
@@ -16,23 +16,10 @@ module OmniAI
|
|
16
16
|
|
17
17
|
required %i[selector]
|
18
18
|
|
19
|
-
# @param to [String] The URL to navigate to.
|
20
19
|
def execute(selector:)
|
21
20
|
@logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
return { error: "unknown selector=#{selector}" } if element.nil?
|
26
|
-
|
27
|
-
element.click
|
28
|
-
end
|
29
|
-
|
30
|
-
protected
|
31
|
-
|
32
|
-
# @return [Watir::Anchor, nil]
|
33
|
-
def find(selector)
|
34
|
-
element = @browser.button(selector)
|
35
|
-
element if element.exists?
|
22
|
+
@driver.button_click(selector:)
|
36
23
|
end
|
37
24
|
end
|
38
25
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
# @example
|
7
|
+
# browser = Watir::Browser.new(:chrome)
|
8
|
+
# tool = OmniAI::Tools::Browser::ElementClickTool.new(browser:)
|
9
|
+
# tool.execute(selector: "#some-id")
|
10
|
+
# tool.execute(selector: ".some-class")
|
11
|
+
# tool.execute(selector: "some text")
|
12
|
+
# tool.execute(selector: "//div[@role='button']")
|
13
|
+
class ElementClickTool < BaseTool
|
14
|
+
description "A browser automation tool for clicking any clickable element."
|
15
|
+
|
16
|
+
parameter :selector, :string,
|
17
|
+
description: "CSS selector, ID, text content, or other identifier for the element to click."
|
18
|
+
|
19
|
+
required %i[selector]
|
20
|
+
|
21
|
+
# @param selector [String] CSS selector, ID, text content, or other identifier for the element to click.
|
22
|
+
def execute(selector:)
|
23
|
+
@logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
|
24
|
+
|
25
|
+
@driver.element_click(selector:)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Elements
|
7
|
+
# Groups HTML elements by their relevance for data entry
|
8
|
+
module ElementGrouper
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def group_for_data_entry(elements, text)
|
12
|
+
groups = {
|
13
|
+
inputs: [], # text, number, email inputs - primary targets
|
14
|
+
form_controls: {}, # radio buttons, checkboxes grouped by name
|
15
|
+
labels: [], # labels, headers, spans
|
16
|
+
actions: [], # buttons and actionable elements
|
17
|
+
}
|
18
|
+
|
19
|
+
elements.each do |element|
|
20
|
+
categorize_element(element, groups, text)
|
21
|
+
end
|
22
|
+
|
23
|
+
groups
|
24
|
+
end
|
25
|
+
|
26
|
+
def categorize_element(element, groups, text)
|
27
|
+
case element.name.downcase
|
28
|
+
when "input"
|
29
|
+
categorize_input_element(element, groups)
|
30
|
+
when "textarea", "select"
|
31
|
+
groups[:inputs] << element
|
32
|
+
when "button"
|
33
|
+
groups[:actions] << element if contains_text_match?(element, text)
|
34
|
+
when "label", "span", "div", "th"
|
35
|
+
groups[:labels] << element if contains_text_match?(element, text)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def categorize_input_element(element, groups)
|
40
|
+
if data_entry_input?(element)
|
41
|
+
groups[:inputs] << element
|
42
|
+
else
|
43
|
+
group_form_control(element, groups[:form_controls])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def data_entry_input?(element)
|
48
|
+
# Handle missing type attribute - HTML default is "text"
|
49
|
+
type = (element["type"] || "text").downcase
|
50
|
+
|
51
|
+
# Include common data entry input types
|
52
|
+
%w[text number email tel url date datetime-local time month week
|
53
|
+
password search].include?(type)
|
54
|
+
end
|
55
|
+
|
56
|
+
def group_form_control(element, form_controls)
|
57
|
+
type = (element["type"] || "text").downcase
|
58
|
+
return unless %w[radio checkbox].include?(type)
|
59
|
+
|
60
|
+
name = element["name"] || "unnamed"
|
61
|
+
form_controls[name] ||= []
|
62
|
+
form_controls[name] << element
|
63
|
+
end
|
64
|
+
|
65
|
+
def contains_text_match?(element, text)
|
66
|
+
element.text.downcase.include?(text.downcase) ||
|
67
|
+
element["value"]&.downcase&.include?(text.downcase)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Elements
|
7
|
+
# Handles detection of interactive elements near text matches
|
8
|
+
module NearbyElementDetector
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def add_nearby_interactive_elements(elements)
|
12
|
+
nearby_elements = []
|
13
|
+
elements.each do |element|
|
14
|
+
nearby_elements += find_interactive_in_context(element)
|
15
|
+
end
|
16
|
+
combined = elements.to_a + nearby_elements
|
17
|
+
combined.uniq
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_interactive_in_context(element)
|
21
|
+
interactive_elements = []
|
22
|
+
interactive_elements += check_table_context(element)
|
23
|
+
interactive_elements += check_parent_containers(element)
|
24
|
+
interactive_elements.uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_table_context(element)
|
28
|
+
elements = []
|
29
|
+
table_cell = element.ancestors("td").first || element.ancestors("th").first
|
30
|
+
elements += find_interactive_in_container(table_cell) if table_cell
|
31
|
+
table_row = element.ancestors("tr").first
|
32
|
+
elements += find_interactive_in_container(table_row) if table_row
|
33
|
+
if table_cell && table_row
|
34
|
+
table = element.ancestors("table").first
|
35
|
+
elements += find_interactive_in_same_column(table, table_cell, table_row) if table
|
36
|
+
end
|
37
|
+
elements
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_interactive_in_same_column(table, header_cell, header_row)
|
41
|
+
return [] unless table
|
42
|
+
|
43
|
+
column_index = header_row.css("th, td").index(header_cell)
|
44
|
+
return [] unless column_index
|
45
|
+
|
46
|
+
interactive_elements = []
|
47
|
+
table.css("tbody tr").each do |row|
|
48
|
+
interactive_elements += find_column_cell_elements(row, column_index)
|
49
|
+
end
|
50
|
+
interactive_elements
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_column_cell_elements(row, column_index)
|
54
|
+
target_cell = row.css("td, th")[column_index]
|
55
|
+
return [] unless target_cell
|
56
|
+
|
57
|
+
find_interactive_in_container(target_cell) + find_interactive_in_nested_tables(target_cell, column_index)
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_interactive_in_nested_tables(cell, original_column_index)
|
61
|
+
interactive_elements = []
|
62
|
+
|
63
|
+
cell.css("table").each do |nested_table|
|
64
|
+
nested_table.css("tr").each do |nested_row|
|
65
|
+
nested_cells = nested_row.css("td, th")
|
66
|
+
if nested_cells[original_column_index]
|
67
|
+
interactive_elements += find_interactive_in_container(nested_cells[original_column_index])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
interactive_elements
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_parent_containers(element)
|
76
|
+
interactive_elements = []
|
77
|
+
parent_container = element.parent
|
78
|
+
3.times do
|
79
|
+
break unless parent_container
|
80
|
+
|
81
|
+
interactive_elements += find_interactive_in_container(parent_container)
|
82
|
+
parent_container = parent_container.parent
|
83
|
+
end
|
84
|
+
interactive_elements
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_interactive_in_container(container)
|
88
|
+
return [] unless container
|
89
|
+
|
90
|
+
container.css("input, textarea, select, button, [role='button'], [tabindex='0']")
|
91
|
+
.reject { |el| non_interactive_element?(el) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def non_interactive_element?(element)
|
95
|
+
return true if element["type"] == "hidden"
|
96
|
+
return true if element["tabindex"] == "-1"
|
97
|
+
return true if element["aria-hidden"] == "true"
|
98
|
+
|
99
|
+
style = element["style"]
|
100
|
+
return true if style&.include?("display: none") || style&.include?("visibility: hidden")
|
101
|
+
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Formatters
|
7
|
+
# Handles formatting of action elements (buttons, links)
|
8
|
+
module ActionFormatter
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def format_actions(actions)
|
12
|
+
result = "⚡ Available Actions:\n"
|
13
|
+
actions.first(5).each do |action|
|
14
|
+
result += format_action_element(action)
|
15
|
+
end
|
16
|
+
result += " ... and #{actions.size - 5} more\n" if actions.size > 5
|
17
|
+
result += "\n"
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_action_element(action)
|
21
|
+
text = action.text.strip
|
22
|
+
selector = get_action_selector(action)
|
23
|
+
" • #{text} (#{selector})\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_action_selector(action)
|
27
|
+
return action["id"] if action["id"] && !action["id"].empty?
|
28
|
+
return "text:#{action.text.strip}" if action.text.strip.length > 2
|
29
|
+
return action["class"].split.first if action["class"]
|
30
|
+
|
31
|
+
"css-needed"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Formatters
|
7
|
+
# Formats grouped elements for optimal data entry display
|
8
|
+
module DataEntryFormatter
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def format_groups(grouped_elements, total_count, text)
|
12
|
+
result = build_header(total_count, text)
|
13
|
+
result += format_each_group(grouped_elements)
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
def format_inputs(inputs)
|
18
|
+
result = "📝 Data Entry Fields:\n"
|
19
|
+
table_groups = group_by_table_context(inputs)
|
20
|
+
result += format_table_groups(table_groups)
|
21
|
+
"#{result}\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_form_controls(form_controls)
|
25
|
+
result = "🎛️ Form Controls:\n"
|
26
|
+
form_controls.each do |name, controls|
|
27
|
+
result += format_control_group(name, controls)
|
28
|
+
end
|
29
|
+
result += "\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_labels(labels)
|
33
|
+
result = "🏷️ Labels & Headers:\n"
|
34
|
+
labels.first(3).each do |label|
|
35
|
+
result += " • #{label.name}: #{label.text.strip}\n"
|
36
|
+
end
|
37
|
+
result += " ... and #{labels.size - 3} more\n" if labels.size > 3
|
38
|
+
result += "\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_header(total_count, text)
|
42
|
+
"Found #{total_count} elements containing '#{text}':\n\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_each_group(grouped_elements)
|
46
|
+
[
|
47
|
+
format_inputs_section(grouped_elements[:inputs]),
|
48
|
+
format_actions_section(grouped_elements[:actions]),
|
49
|
+
format_controls_section(grouped_elements[:form_controls]),
|
50
|
+
format_labels_section(grouped_elements[:labels]),
|
51
|
+
].join
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_inputs_section(inputs)
|
55
|
+
inputs.any? ? format_inputs(inputs) : ""
|
56
|
+
end
|
57
|
+
|
58
|
+
def format_actions_section(actions)
|
59
|
+
actions.any? ? ActionFormatter.format_actions(actions) : ""
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_controls_section(form_controls)
|
63
|
+
form_controls.any? ? format_form_controls(form_controls) : ""
|
64
|
+
end
|
65
|
+
|
66
|
+
def format_labels_section(labels)
|
67
|
+
labels.any? ? format_labels(labels) : ""
|
68
|
+
end
|
69
|
+
|
70
|
+
def format_control_group(name, controls)
|
71
|
+
result = "\n #{name.humanize} options:\n"
|
72
|
+
controls.each do |control|
|
73
|
+
checked = control["checked"] ? " \u2713" : ""
|
74
|
+
result += " • #{control['value']}#{checked}\n"
|
75
|
+
end
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_table_groups(table_groups)
|
80
|
+
return format_single_group(table_groups.values.first) if table_groups.size == 1
|
81
|
+
|
82
|
+
result = ""
|
83
|
+
table_groups.each do |(context, group_inputs)|
|
84
|
+
result += "\n #{context}:\n"
|
85
|
+
group_inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
def format_single_group(inputs)
|
91
|
+
result = ""
|
92
|
+
inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def group_by_table_context(inputs)
|
97
|
+
groups = {}
|
98
|
+
inputs.each do |input|
|
99
|
+
context = find_context(input)
|
100
|
+
groups[context] ||= []
|
101
|
+
groups[context] << input
|
102
|
+
end
|
103
|
+
groups
|
104
|
+
end
|
105
|
+
|
106
|
+
def find_context(input)
|
107
|
+
table = input.ancestors("table").first
|
108
|
+
return "Form Fields" unless table
|
109
|
+
|
110
|
+
find_table_context(input)
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_table_context(input)
|
114
|
+
row = input.ancestors("tr").first
|
115
|
+
return "Table" unless row
|
116
|
+
|
117
|
+
find_meaningful_cell_text(row)
|
118
|
+
end
|
119
|
+
|
120
|
+
def find_meaningful_cell_text(row)
|
121
|
+
meaningful_cell = row.css("td, th").find do |cell|
|
122
|
+
cell.text.strip.length > 2 && !cell.text.match?(/^\d+\.?\d*$/)
|
123
|
+
end
|
124
|
+
|
125
|
+
meaningful_cell ? meaningful_cell.text.strip[0..30] : "Table"
|
126
|
+
end
|
127
|
+
|
128
|
+
def humanize(text)
|
129
|
+
text.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Formatters
|
7
|
+
# Handles formatting of HTML elements for display with data entry focus
|
8
|
+
module ElementFormatter
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def format_matching_elements(elements, text, _context_size = nil)
|
12
|
+
grouped = Elements::ElementGrouper.group_for_data_entry(elements, text)
|
13
|
+
DataEntryFormatter.format_groups(grouped, elements.size, text)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Keep existing methods for backward compatibility
|
17
|
+
def format_single_element(element, index, context_size)
|
18
|
+
result = "--- Element #{index + 1} ---\n"
|
19
|
+
result += "Tag: #{element.name}\n"
|
20
|
+
result += format_element_attributes(element)
|
21
|
+
result += get_parent_context(element, context_size) if context_size.positive?
|
22
|
+
result += "HTML: #{element.to_html}\n\n"
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_element_attributes(element)
|
27
|
+
result = ""
|
28
|
+
result += "ID: #{element['id']}\n" if element["id"]
|
29
|
+
result += "Classes: #{element['class']}\n" if element["class"]
|
30
|
+
%w[href src alt type value placeholder].each do |attr|
|
31
|
+
result += "#{attr}: #{element[attr]}\n" if element[attr] && !element[attr].empty?
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_parent_context(element, context_size)
|
37
|
+
result = ""
|
38
|
+
parent = element.parent
|
39
|
+
context_count = 0
|
40
|
+
while parent && context_count < context_size
|
41
|
+
attrs = parent.attributes.map { |name, attr| " #{name}=\"#{attr.value}\"" }.join
|
42
|
+
result += "Parent #{context_count + 1}: <#{parent.name}#{attrs}>\n"
|
43
|
+
parent = parent.parent
|
44
|
+
context_count += 1
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OmniAI
|
4
|
+
module Tools
|
5
|
+
module Browser
|
6
|
+
module Formatters
|
7
|
+
# Handles formatting of input elements
|
8
|
+
module InputFormatter
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def format_input_field(input, indent = "")
|
12
|
+
result = format_basic_line(input, indent)
|
13
|
+
result += format_selectors_line(input, indent)
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
def format_basic_line(input, indent)
|
18
|
+
result = "#{indent}• #{input_type_display(input)}"
|
19
|
+
result += input_id_display(input)
|
20
|
+
result += input_value_display(input)
|
21
|
+
result += input_placeholder_display(input)
|
22
|
+
"#{result}\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
def format_selectors_line(input, indent)
|
26
|
+
selectors = SelectorGenerator.generate_stable_selectors(input)
|
27
|
+
return "" if selectors.empty?
|
28
|
+
|
29
|
+
format_selector_list(selectors, indent)
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_selector_list(selectors, indent)
|
33
|
+
result = "#{indent} Stable selectors:\n"
|
34
|
+
selectors.each { |sel| result += "#{indent} - #{sel}\n" }
|
35
|
+
"#{result}\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def input_type_display(input)
|
39
|
+
(input["type"] || input.name).capitalize
|
40
|
+
end
|
41
|
+
|
42
|
+
def input_id_display(input)
|
43
|
+
input["id"] ? " (#{input['id']})" : ""
|
44
|
+
end
|
45
|
+
|
46
|
+
def input_value_display(input)
|
47
|
+
value = input["value"]
|
48
|
+
value && !value.empty? ? " = '#{value}'" : ""
|
49
|
+
end
|
50
|
+
|
51
|
+
def input_placeholder_display(input)
|
52
|
+
placeholder = input["placeholder"]
|
53
|
+
placeholder && !placeholder.empty? ? " [#{placeholder}]" : ""
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|