shared_tools 0.3.1 → 0.4.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/CHANGELOG.md +46 -16
- data/README.md +257 -262
- data/lib/shared_tools/browser_tool.rb +5 -0
- data/lib/shared_tools/calculator_tool.rb +4 -0
- data/lib/shared_tools/clipboard_tool.rb +4 -0
- data/lib/shared_tools/composite_analysis_tool.rb +4 -0
- data/lib/shared_tools/computer_tool.rb +5 -0
- data/lib/shared_tools/cron_tool.rb +4 -0
- data/lib/shared_tools/current_date_time_tool.rb +4 -0
- data/lib/shared_tools/data_science_kit.rb +4 -0
- data/lib/shared_tools/database.rb +4 -0
- data/lib/shared_tools/database_query_tool.rb +4 -0
- data/lib/shared_tools/database_tool.rb +5 -0
- data/lib/shared_tools/disk_tool.rb +5 -0
- data/lib/shared_tools/dns_tool.rb +4 -0
- data/lib/shared_tools/doc_tool.rb +5 -0
- data/lib/shared_tools/error_handling_tool.rb +4 -0
- data/lib/shared_tools/eval_tool.rb +5 -0
- data/lib/shared_tools/mcp/brave_search_client.rb +37 -0
- data/lib/shared_tools/mcp/chart_client.rb +32 -0
- data/lib/shared_tools/mcp/github_client.rb +38 -0
- data/lib/shared_tools/mcp/hugging_face_client.rb +43 -0
- data/lib/shared_tools/mcp/memory_client.rb +33 -0
- data/lib/shared_tools/mcp/notion_client.rb +40 -0
- data/lib/shared_tools/mcp/sequential_thinking_client.rb +33 -0
- data/lib/shared_tools/mcp/slack_client.rb +54 -0
- data/lib/shared_tools/mcp/streamable_http_patch.rb +42 -0
- data/lib/shared_tools/mcp/tavily_client.rb +41 -0
- data/lib/shared_tools/mcp.rb +45 -16
- data/lib/shared_tools/system_info_tool.rb +4 -0
- data/lib/shared_tools/tools/browser/base_tool.rb +8 -12
- data/lib/shared_tools/tools/browser/click_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/ferrum_driver.rb +119 -0
- data/lib/shared_tools/tools/browser/inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +19 -7
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +4 -2
- data/lib/shared_tools/tools/browser/visit_tool.rb +4 -2
- data/lib/shared_tools/tools/browser.rb +31 -2
- data/lib/shared_tools/tools/browser_tool.rb +6 -0
- data/lib/shared_tools/tools/clipboard_tool.rb +69 -144
- data/lib/shared_tools/tools/composite_analysis_tool.rb +60 -4
- data/lib/shared_tools/tools/computer/mac_driver.rb +37 -4
- data/lib/shared_tools/tools/cron_tool.rb +237 -379
- data/lib/shared_tools/tools/current_date_time_tool.rb +54 -120
- data/lib/shared_tools/tools/data_science_kit.rb +63 -13
- data/lib/shared_tools/tools/dns_tool.rb +335 -269
- data/lib/shared_tools/tools/doc/docx_reader_tool.rb +107 -0
- data/lib/shared_tools/tools/doc/spreadsheet_reader_tool.rb +171 -0
- data/lib/shared_tools/tools/doc/text_reader_tool.rb +57 -0
- data/lib/shared_tools/tools/doc.rb +3 -0
- data/lib/shared_tools/tools/doc_tool.rb +101 -6
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +1 -1
- data/lib/shared_tools/tools/enabler.rb +42 -0
- data/lib/shared_tools/tools/error_handling_tool.rb +3 -1
- data/lib/shared_tools/tools/notification/base_driver.rb +51 -0
- data/lib/shared_tools/tools/notification/linux_driver.rb +115 -0
- data/lib/shared_tools/tools/notification/mac_driver.rb +66 -0
- data/lib/shared_tools/tools/notification/null_driver.rb +29 -0
- data/lib/shared_tools/tools/notification.rb +12 -0
- data/lib/shared_tools/tools/notification_tool.rb +99 -0
- data/lib/shared_tools/tools/system_info_tool.rb +130 -343
- data/lib/shared_tools/tools/workflow_manager_tool.rb +32 -0
- data/lib/shared_tools/utilities.rb +193 -0
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools/weather_tool.rb +4 -0
- data/lib/shared_tools/workflow_manager_tool.rb +4 -0
- data/lib/shared_tools.rb +28 -38
- metadata +74 -9
- data/lib/shared_tools/mcp/github_mcp_server.rb +0 -58
- data/lib/shared_tools/mcp/imcp.rb +0 -28
- data/lib/shared_tools/mcp/tavily_mcp_server.rb +0 -44
- data/lib/shared_tools/tools/devops_toolkit.rb +0 -420
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "docx"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# docx is optional - will raise error when tool is used without it
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module SharedTools
|
|
10
|
+
module Tools
|
|
11
|
+
module Doc
|
|
12
|
+
# Read text content from Microsoft Word (.docx) documents.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# tool = SharedTools::Tools::Doc::DocxReaderTool.new
|
|
16
|
+
# tool.execute(doc_path: "./report.docx")
|
|
17
|
+
# tool.execute(doc_path: "./report.docx", paragraph_range: "1-10")
|
|
18
|
+
class DocxReaderTool < ::RubyLLM::Tool
|
|
19
|
+
def self.name = 'doc_docx_read'
|
|
20
|
+
|
|
21
|
+
description "Read the text content of a Microsoft Word (.docx) document."
|
|
22
|
+
|
|
23
|
+
params do
|
|
24
|
+
string :doc_path, description: "Path to the .docx file."
|
|
25
|
+
|
|
26
|
+
string :paragraph_range, description: <<~DESC.strip, required: false
|
|
27
|
+
Optional range of paragraphs to extract, 1-based.
|
|
28
|
+
Accepts the same notation as pdf_read page numbers:
|
|
29
|
+
- Single paragraph: "5"
|
|
30
|
+
- Multiple paragraphs: "1, 3, 5"
|
|
31
|
+
- Range: "1-20"
|
|
32
|
+
- Mixed: "1, 5-10, 15"
|
|
33
|
+
Omit to return the full document.
|
|
34
|
+
DESC
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param logger [Logger] optional logger
|
|
38
|
+
def initialize(logger: nil)
|
|
39
|
+
@logger = logger || RubyLLM.logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param doc_path [String] path to .docx file
|
|
43
|
+
# @param paragraph_range [String, nil] optional paragraph range
|
|
44
|
+
# @return [Hash] extraction result
|
|
45
|
+
def execute(doc_path:, paragraph_range: nil)
|
|
46
|
+
raise LoadError, "DocxReaderTool requires the 'docx' gem. Install it with: gem install docx" unless defined?(Docx)
|
|
47
|
+
|
|
48
|
+
@logger.info("DocxReaderTool#execute doc_path=#{doc_path} paragraph_range=#{paragraph_range}")
|
|
49
|
+
|
|
50
|
+
unless File.exist?(doc_path)
|
|
51
|
+
return { error: "File not found: #{doc_path}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
unless File.extname(doc_path).downcase == '.docx'
|
|
55
|
+
return { error: "Expected a .docx file, got: #{File.extname(doc_path)}" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
doc = Docx::Document.open(doc_path)
|
|
59
|
+
paragraphs = doc.paragraphs.map(&:to_s).reject { |p| p.strip.empty? }
|
|
60
|
+
total = paragraphs.length
|
|
61
|
+
|
|
62
|
+
@logger.debug("Loaded #{total} non-empty paragraphs from #{doc_path}")
|
|
63
|
+
|
|
64
|
+
selected_indices = if paragraph_range
|
|
65
|
+
parse_range(paragraph_range, total)
|
|
66
|
+
else
|
|
67
|
+
(1..total).to_a
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
invalid = selected_indices.select { |n| n < 1 || n > total }
|
|
71
|
+
valid = selected_indices.select { |n| n >= 1 && n <= total }
|
|
72
|
+
|
|
73
|
+
extracted = valid.map { |n| { paragraph: n, text: paragraphs[n - 1] } }
|
|
74
|
+
|
|
75
|
+
@logger.info("Extracted #{extracted.size} paragraphs from #{doc_path}")
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
doc_path: doc_path,
|
|
79
|
+
total_paragraphs: total,
|
|
80
|
+
requested_range: paragraph_range || "all",
|
|
81
|
+
invalid_paragraphs: invalid,
|
|
82
|
+
paragraphs: extracted,
|
|
83
|
+
full_text: extracted.map { |p| p[:text] }.join("\n\n")
|
|
84
|
+
}
|
|
85
|
+
rescue => e
|
|
86
|
+
@logger.error("Failed to read DOCX '#{doc_path}': #{e.message}")
|
|
87
|
+
{ error: e.message }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Parse a range string like "1, 3-5, 10" into a sorted array of integers.
|
|
93
|
+
def parse_range(range_str, max)
|
|
94
|
+
range_str.split(',').flat_map do |part|
|
|
95
|
+
part.strip!
|
|
96
|
+
if part.include?('-')
|
|
97
|
+
lo, hi = part.split('-').map { |n| n.strip.to_i }
|
|
98
|
+
(lo..hi).to_a
|
|
99
|
+
else
|
|
100
|
+
[part.to_i]
|
|
101
|
+
end
|
|
102
|
+
end.uniq.sort
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "roo"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# roo is optional - will raise an error when the tool is used without it
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module SharedTools
|
|
10
|
+
module Tools
|
|
11
|
+
module Doc
|
|
12
|
+
# Read spreadsheet data from CSV, XLSX, ODS, and other formats supported by the roo gem.
|
|
13
|
+
#
|
|
14
|
+
# @example Read all rows from a CSV
|
|
15
|
+
# tool = SharedTools::Tools::Doc::SpreadsheetReaderTool.new
|
|
16
|
+
# tool.execute(doc_path: "./data.csv")
|
|
17
|
+
#
|
|
18
|
+
# @example Read a specific sheet from an Excel workbook
|
|
19
|
+
# tool.execute(doc_path: "./report.xlsx", sheet: "Q1 Sales")
|
|
20
|
+
#
|
|
21
|
+
# @example Read a row range from a worksheet
|
|
22
|
+
# tool.execute(doc_path: "./report.xlsx", sheet: "Summary", row_range: "2-50")
|
|
23
|
+
class SpreadsheetReaderTool < ::RubyLLM::Tool
|
|
24
|
+
def self.name = 'doc_spreadsheet_read'
|
|
25
|
+
|
|
26
|
+
description "Read tabular data from spreadsheet files (CSV, XLSX, ODS, and other formats)."
|
|
27
|
+
|
|
28
|
+
SUPPORTED_FORMATS = %w[.csv .xlsx .ods .xlsm].freeze
|
|
29
|
+
|
|
30
|
+
params do
|
|
31
|
+
string :doc_path, description: <<~DESC.strip
|
|
32
|
+
Path to the spreadsheet file. Supported formats:
|
|
33
|
+
- .csv — Comma-separated values (plain text, no gem beyond roo required)
|
|
34
|
+
- .xlsx — Microsoft Excel Open XML Workbook (Excel 2007+)
|
|
35
|
+
- .xlsm — Microsoft Excel Macro-Enabled Workbook
|
|
36
|
+
- .ods — OpenDocument Spreadsheet (LibreOffice / OpenOffice)
|
|
37
|
+
Note: Legacy .xls (Excel 97-2003) requires the additional 'roo-xls' gem.
|
|
38
|
+
DESC
|
|
39
|
+
|
|
40
|
+
string :sheet, description: <<~DESC.strip, required: false
|
|
41
|
+
Name or 1-based index of the worksheet to read. For multi-sheet workbooks
|
|
42
|
+
(XLSX, ODS), specify the exact sheet name (e.g. "Q1 Sales") or a number
|
|
43
|
+
(e.g. "2" for the second sheet). Defaults to the first sheet.
|
|
44
|
+
CSV files always have a single implicit sheet called "default".
|
|
45
|
+
DESC
|
|
46
|
+
|
|
47
|
+
string :row_range, description: <<~DESC.strip, required: false
|
|
48
|
+
Row range to extract, 1-based (row 1 is the header row in most spreadsheets).
|
|
49
|
+
Accepts the same notation as other doc tool parameters:
|
|
50
|
+
- Single row: "3"
|
|
51
|
+
- Multiple rows: "1, 3, 5"
|
|
52
|
+
- Range: "1-100"
|
|
53
|
+
- Mixed: "1, 5-20, 30"
|
|
54
|
+
Omit to return all rows.
|
|
55
|
+
DESC
|
|
56
|
+
|
|
57
|
+
boolean :headers, description: <<~DESC.strip, required: false
|
|
58
|
+
When true (default), treats the first row as column headers and returns
|
|
59
|
+
each subsequent row as a hash keyed by header name. When false, returns
|
|
60
|
+
each row as a plain array of values. Set to false when the spreadsheet
|
|
61
|
+
has no header row or when you want raw positional data.
|
|
62
|
+
DESC
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param logger [Logger] optional logger
|
|
66
|
+
def initialize(logger: nil)
|
|
67
|
+
@logger = logger || RubyLLM.logger
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param doc_path [String] path to spreadsheet file
|
|
71
|
+
# @param sheet [String, nil] sheet name or 1-based index
|
|
72
|
+
# @param row_range [String, nil] row range to extract
|
|
73
|
+
# @param headers [Boolean] whether first row is headers
|
|
74
|
+
# @return [Hash] extraction result
|
|
75
|
+
def execute(doc_path:, sheet: nil, row_range: nil, headers: true)
|
|
76
|
+
raise LoadError, "SpreadsheetReaderTool requires the 'roo' gem. Install it with: gem install roo" unless defined?(Roo)
|
|
77
|
+
|
|
78
|
+
@logger.info("SpreadsheetReaderTool#execute doc_path=#{doc_path} sheet=#{sheet} row_range=#{row_range}")
|
|
79
|
+
|
|
80
|
+
return { error: "File not found: #{doc_path}" } unless File.exist?(doc_path)
|
|
81
|
+
|
|
82
|
+
ext = File.extname(doc_path).downcase
|
|
83
|
+
unless SUPPORTED_FORMATS.include?(ext)
|
|
84
|
+
return { error: "Unsupported format '#{ext}'. Supported: #{SUPPORTED_FORMATS.join(', ')}" }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
ss = Roo::Spreadsheet.open(doc_path)
|
|
88
|
+
|
|
89
|
+
# Select sheet
|
|
90
|
+
active_sheet = resolve_sheet(ss, sheet)
|
|
91
|
+
return active_sheet if active_sheet.is_a?(Hash) && active_sheet[:error]
|
|
92
|
+
|
|
93
|
+
ss.default_sheet = active_sheet
|
|
94
|
+
|
|
95
|
+
total_rows = ss.last_row.to_i
|
|
96
|
+
first_row = ss.first_row.to_i
|
|
97
|
+
header_row = headers ? ss.row(first_row).map { |h| h.to_s.strip } : nil
|
|
98
|
+
|
|
99
|
+
# Determine data rows (skip header if using headers)
|
|
100
|
+
data_start = headers ? first_row + 1 : first_row
|
|
101
|
+
all_indices = (data_start..total_rows).to_a
|
|
102
|
+
|
|
103
|
+
selected = row_range ? filter_rows(all_indices, row_range) : all_indices
|
|
104
|
+
invalid = selected.reject { |n| n >= first_row && n <= total_rows }
|
|
105
|
+
valid = selected.select { |n| n >= first_row && n <= total_rows }
|
|
106
|
+
|
|
107
|
+
rows = valid.map do |n|
|
|
108
|
+
raw = ss.row(n)
|
|
109
|
+
if headers && header_row
|
|
110
|
+
header_row.zip(raw).to_h
|
|
111
|
+
else
|
|
112
|
+
raw
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@logger.info("SpreadsheetReaderTool: read #{rows.size} rows from '#{active_sheet}'")
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
doc_path: doc_path,
|
|
120
|
+
format: ext,
|
|
121
|
+
available_sheets: ss.sheets,
|
|
122
|
+
active_sheet: active_sheet,
|
|
123
|
+
total_rows: total_rows,
|
|
124
|
+
header_row: header_row,
|
|
125
|
+
requested_range: row_range || "all",
|
|
126
|
+
invalid_rows: invalid,
|
|
127
|
+
row_count: rows.size,
|
|
128
|
+
rows: rows
|
|
129
|
+
}
|
|
130
|
+
rescue => e
|
|
131
|
+
@logger.error("SpreadsheetReaderTool failed for '#{doc_path}': #{e.message}")
|
|
132
|
+
{ error: e.message }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Resolve sheet name or 1-based index to the actual sheet name.
|
|
138
|
+
def resolve_sheet(ss, sheet_param)
|
|
139
|
+
return ss.sheets.first if sheet_param.nil?
|
|
140
|
+
|
|
141
|
+
# Numeric string → treat as 1-based index
|
|
142
|
+
if sheet_param.match?(/\A\d+\z/)
|
|
143
|
+
idx = sheet_param.to_i - 1
|
|
144
|
+
return { error: "Sheet index #{sheet_param} out of range (workbook has #{ss.sheets.size} sheet(s))" } if idx < 0 || idx >= ss.sheets.size
|
|
145
|
+
return ss.sheets[idx]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Named sheet
|
|
149
|
+
return sheet_param if ss.sheets.include?(sheet_param)
|
|
150
|
+
|
|
151
|
+
{ error: "Sheet '#{sheet_param}' not found. Available sheets: #{ss.sheets.join(', ')}" }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Parse a range string like "1, 3-5, 10" into a sorted array of row indices.
|
|
155
|
+
def filter_rows(all_indices, range_str)
|
|
156
|
+
requested = range_str.split(',').flat_map do |part|
|
|
157
|
+
part.strip!
|
|
158
|
+
if part.include?('-')
|
|
159
|
+
lo, hi = part.split('-').map { |n| n.strip.to_i }
|
|
160
|
+
(lo..hi).to_a
|
|
161
|
+
else
|
|
162
|
+
[part.to_i]
|
|
163
|
+
end
|
|
164
|
+
end.uniq.sort
|
|
165
|
+
|
|
166
|
+
all_indices & requested
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Doc
|
|
6
|
+
# Read and return the full contents of a plain text file.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# tool = SharedTools::Tools::Doc::TextReaderTool.new
|
|
10
|
+
# tool.execute(doc_path: "./guide.txt")
|
|
11
|
+
class TextReaderTool < ::RubyLLM::Tool
|
|
12
|
+
def self.name = 'doc_text_read'
|
|
13
|
+
|
|
14
|
+
description "Read the full contents of a plain text file."
|
|
15
|
+
|
|
16
|
+
params do
|
|
17
|
+
string :doc_path, description: "Path to the text file to read."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param logger [Logger] optional logger
|
|
21
|
+
def initialize(logger: nil)
|
|
22
|
+
@logger = logger || RubyLLM.logger
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param doc_path [String] path to the text file
|
|
26
|
+
# @return [Hash] file content and metadata
|
|
27
|
+
def execute(doc_path:)
|
|
28
|
+
@logger.info("TextReaderTool#execute doc_path=#{doc_path.inspect}")
|
|
29
|
+
|
|
30
|
+
raise ArgumentError, "doc_path is required" if doc_path.nil? || doc_path.strip.empty?
|
|
31
|
+
raise ArgumentError, "File not found: #{doc_path}" unless File.exist?(doc_path)
|
|
32
|
+
raise ArgumentError, "Not a file: #{doc_path}" unless File.file?(doc_path)
|
|
33
|
+
|
|
34
|
+
content = File.read(doc_path, encoding: 'utf-8')
|
|
35
|
+
line_count = content.lines.size
|
|
36
|
+
char_count = content.length
|
|
37
|
+
word_count = content.split.size
|
|
38
|
+
|
|
39
|
+
@logger.info("TextReaderTool read #{char_count} chars, #{line_count} lines from #{doc_path}")
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
doc_path: doc_path,
|
|
43
|
+
content: content,
|
|
44
|
+
line_count: line_count,
|
|
45
|
+
word_count: word_count,
|
|
46
|
+
char_count: char_count
|
|
47
|
+
}
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
raise
|
|
50
|
+
rescue => e
|
|
51
|
+
@logger.error("TextReaderTool failed to read #{doc_path}: #{e.message}")
|
|
52
|
+
{ error: e.message, doc_path: doc_path }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -9,19 +9,28 @@ module SharedTools
|
|
|
9
9
|
def self.name = 'doc_tool'
|
|
10
10
|
|
|
11
11
|
module Action
|
|
12
|
-
PDF_READ
|
|
12
|
+
PDF_READ = "pdf_read"
|
|
13
|
+
TEXT_READ = "text_read"
|
|
14
|
+
DOCX_READ = "docx_read"
|
|
15
|
+
SPREADSHEET_READ = "spreadsheet_read"
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
ACTIONS = [
|
|
16
19
|
Action::PDF_READ,
|
|
20
|
+
Action::TEXT_READ,
|
|
21
|
+
Action::DOCX_READ,
|
|
22
|
+
Action::SPREADSHEET_READ,
|
|
17
23
|
].freeze
|
|
18
24
|
|
|
19
25
|
description <<~TEXT
|
|
20
|
-
Read and process
|
|
26
|
+
Read and process document files.
|
|
21
27
|
|
|
22
28
|
## Actions:
|
|
23
29
|
|
|
24
|
-
1. `#{Action::
|
|
30
|
+
1. `#{Action::TEXT_READ}` - Read the full contents of a plain text file (.txt, .md, etc.)
|
|
31
|
+
Required: "action": "text_read", "doc_path": "[path to text file]"
|
|
32
|
+
|
|
33
|
+
2. `#{Action::PDF_READ}` - Read specific pages from a PDF document
|
|
25
34
|
Required: "action": "pdf_read", "doc_path": "[path to PDF]", "page_numbers": "[comma-separated page numbers]"
|
|
26
35
|
|
|
27
36
|
The page_numbers parameter accepts:
|
|
@@ -29,8 +38,18 @@ module SharedTools
|
|
|
29
38
|
- Multiple pages: "1, 3, 5"
|
|
30
39
|
- Range notation: "1-10" or "1, 3-5, 10"
|
|
31
40
|
|
|
41
|
+
3. `#{Action::DOCX_READ}` - Read text content from a Microsoft Word (.docx) document
|
|
42
|
+
Required: "action": "docx_read", "doc_path": "[path to .docx file]"
|
|
43
|
+
Optional: "paragraph_range": "[comma-separated paragraph numbers or ranges]"
|
|
44
|
+
|
|
45
|
+
The paragraph_range parameter accepts the same notation as page_numbers.
|
|
46
|
+
Omit paragraph_range to return the full document.
|
|
47
|
+
|
|
32
48
|
## Examples:
|
|
33
49
|
|
|
50
|
+
Read a text file
|
|
51
|
+
{"action": "#{Action::TEXT_READ}", "doc_path": "./notes.txt"}
|
|
52
|
+
|
|
34
53
|
Read single page from PDF
|
|
35
54
|
{"action": "#{Action::PDF_READ}", "doc_path": "./document.pdf", "page_numbers": "1"}
|
|
36
55
|
|
|
@@ -42,17 +61,41 @@ module SharedTools
|
|
|
42
61
|
|
|
43
62
|
Read specific pages with range
|
|
44
63
|
{"action": "#{Action::PDF_READ}", "doc_path": "./manual.pdf", "page_numbers": "1, 5-8, 15, 20-25"}
|
|
64
|
+
|
|
65
|
+
Read a full Word document
|
|
66
|
+
{"action": "#{Action::DOCX_READ}", "doc_path": "./report.docx"}
|
|
67
|
+
|
|
68
|
+
Read first 20 paragraphs of a Word document
|
|
69
|
+
{"action": "#{Action::DOCX_READ}", "doc_path": "./report.docx", "paragraph_range": "1-20"}
|
|
70
|
+
|
|
71
|
+
4. `#{Action::SPREADSHEET_READ}` - Read tabular data from a spreadsheet file
|
|
72
|
+
Supported formats: CSV, XLSX, ODS, XLSM
|
|
73
|
+
Required: "action": "spreadsheet_read", "doc_path": "[path to spreadsheet]"
|
|
74
|
+
Optional: "sheet": "[sheet name or 1-based index]"
|
|
75
|
+
"row_range": "[row range, e.g. '2-100']"
|
|
76
|
+
"headers": true/false (default true — first row treated as headers)
|
|
77
|
+
|
|
78
|
+
Read a full CSV file
|
|
79
|
+
{"action": "#{Action::SPREADSHEET_READ}", "doc_path": "./data.csv"}
|
|
80
|
+
|
|
81
|
+
Read a specific sheet from an Excel workbook
|
|
82
|
+
{"action": "#{Action::SPREADSHEET_READ}", "doc_path": "./report.xlsx", "sheet": "Q1 Sales"}
|
|
83
|
+
|
|
84
|
+
Read rows 2-50 from a worksheet without header treatment
|
|
85
|
+
{"action": "#{Action::SPREADSHEET_READ}", "doc_path": "./report.xlsx", "row_range": "2-50", "headers": false}
|
|
45
86
|
TEXT
|
|
46
87
|
|
|
47
88
|
params do
|
|
48
89
|
string :action, description: <<~TEXT.strip
|
|
49
90
|
The document action to perform. Options:
|
|
91
|
+
* `#{Action::TEXT_READ}`: Read a plain text file
|
|
50
92
|
* `#{Action::PDF_READ}`: Read pages from a PDF document
|
|
93
|
+
* `#{Action::DOCX_READ}`: Read paragraphs from a Microsoft Word (.docx) document
|
|
94
|
+
* `#{Action::SPREADSHEET_READ}`: Read tabular data from a spreadsheet (CSV, XLSX, ODS)
|
|
51
95
|
TEXT
|
|
52
96
|
|
|
53
97
|
string :doc_path, description: <<~TEXT.strip, required: false
|
|
54
|
-
Path to the document file. Required for
|
|
55
|
-
* `#{Action::PDF_READ}`
|
|
98
|
+
Path to the document file. Required for all actions.
|
|
56
99
|
TEXT
|
|
57
100
|
|
|
58
101
|
string :page_numbers, description: <<~TEXT.strip, required: false
|
|
@@ -61,6 +104,34 @@ module SharedTools
|
|
|
61
104
|
Required for the following actions:
|
|
62
105
|
* `#{Action::PDF_READ}`
|
|
63
106
|
TEXT
|
|
107
|
+
|
|
108
|
+
string :paragraph_range, description: <<~TEXT.strip, required: false
|
|
109
|
+
Comma-separated paragraph numbers or ranges to read from a Word document (first paragraph is 1).
|
|
110
|
+
Examples: "1", "1, 3, 5", "1-20", "1, 5-8, 15"
|
|
111
|
+
Optional for the following actions (omit to return the full document):
|
|
112
|
+
* `#{Action::DOCX_READ}`
|
|
113
|
+
TEXT
|
|
114
|
+
|
|
115
|
+
string :sheet, description: <<~TEXT.strip, required: false
|
|
116
|
+
Sheet name or 1-based sheet index to read from a multi-sheet spreadsheet.
|
|
117
|
+
Examples: "Q1 Sales", "2"
|
|
118
|
+
Optional for the following actions (defaults to the first sheet):
|
|
119
|
+
* `#{Action::SPREADSHEET_READ}`
|
|
120
|
+
TEXT
|
|
121
|
+
|
|
122
|
+
string :row_range, description: <<~TEXT.strip, required: false
|
|
123
|
+
Comma-separated row numbers or ranges to read from a spreadsheet (first row is 1).
|
|
124
|
+
Examples: "1", "2-100", "1, 5-20, 30"
|
|
125
|
+
Optional for the following actions (omit to return all rows):
|
|
126
|
+
* `#{Action::SPREADSHEET_READ}`
|
|
127
|
+
TEXT
|
|
128
|
+
|
|
129
|
+
boolean :headers, description: <<~TEXT.strip, required: false
|
|
130
|
+
When true (default), treats the first row as column headers and returns each
|
|
131
|
+
subsequent row as a hash. When false, returns rows as plain arrays.
|
|
132
|
+
Optional for the following actions:
|
|
133
|
+
* `#{Action::SPREADSHEET_READ}`
|
|
134
|
+
TEXT
|
|
64
135
|
end
|
|
65
136
|
|
|
66
137
|
# @param logger [Logger] optional logger
|
|
@@ -73,14 +144,23 @@ module SharedTools
|
|
|
73
144
|
# @param page_numbers [String, nil] page numbers to read
|
|
74
145
|
#
|
|
75
146
|
# @return [Hash] execution result
|
|
76
|
-
def execute(action:, doc_path: nil, page_numbers: nil)
|
|
147
|
+
def execute(action:, doc_path: nil, page_numbers: nil, paragraph_range: nil, sheet: nil, row_range: nil, headers: true)
|
|
77
148
|
@logger.info("DocTool#execute action=#{action}")
|
|
78
149
|
|
|
79
150
|
case action.to_s.downcase
|
|
151
|
+
when Action::TEXT_READ
|
|
152
|
+
require_param!(:doc_path, doc_path)
|
|
153
|
+
text_reader_tool.execute(doc_path: doc_path)
|
|
80
154
|
when Action::PDF_READ
|
|
81
155
|
require_param!(:doc_path, doc_path)
|
|
82
156
|
require_param!(:page_numbers, page_numbers)
|
|
83
157
|
pdf_reader_tool.execute(doc_path: doc_path, page_numbers: page_numbers)
|
|
158
|
+
when Action::DOCX_READ
|
|
159
|
+
require_param!(:doc_path, doc_path)
|
|
160
|
+
docx_reader_tool.execute(doc_path: doc_path, paragraph_range: paragraph_range)
|
|
161
|
+
when Action::SPREADSHEET_READ
|
|
162
|
+
require_param!(:doc_path, doc_path)
|
|
163
|
+
spreadsheet_reader_tool.execute(doc_path: doc_path, sheet: sheet, row_range: row_range, headers: headers)
|
|
84
164
|
else
|
|
85
165
|
{ error: "Unsupported action: #{action}. Supported actions are: #{ACTIONS.join(', ')}" }
|
|
86
166
|
end
|
|
@@ -104,6 +184,21 @@ module SharedTools
|
|
|
104
184
|
def pdf_reader_tool
|
|
105
185
|
@pdf_reader_tool ||= Doc::PdfReaderTool.new(logger: @logger)
|
|
106
186
|
end
|
|
187
|
+
|
|
188
|
+
# @return [Doc::TextReaderTool]
|
|
189
|
+
def text_reader_tool
|
|
190
|
+
@text_reader_tool ||= Doc::TextReaderTool.new(logger: @logger)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @return [Doc::DocxReaderTool]
|
|
194
|
+
def docx_reader_tool
|
|
195
|
+
@docx_reader_tool ||= Doc::DocxReaderTool.new(logger: @logger)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @return [Doc::SpreadsheetReaderTool]
|
|
199
|
+
def spreadsheet_reader_tool
|
|
200
|
+
@spreadsheet_reader_tool ||= Doc::SpreadsheetReaderTool.new(logger: @logger)
|
|
201
|
+
end
|
|
107
202
|
end
|
|
108
203
|
end
|
|
109
204
|
end
|
|
@@ -16,7 +16,7 @@ module SharedTools
|
|
|
16
16
|
params do
|
|
17
17
|
string :service, description: "The service to run the command on (e.g. `app`).", required: false
|
|
18
18
|
string :command, description: "The command to run (e.g. `rspec`)."
|
|
19
|
-
array :args, description: "The arguments for the command.", required: false
|
|
19
|
+
array :args, of: :string, description: "The arguments for the command.", required: false
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# @example
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# lib/shared_tools/tools/enabler.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# Part of an idea to reduce the number of tokens used when there
|
|
4
|
+
# are a large number tools available. As part of the initial prompt
|
|
5
|
+
# send tool names, descriptions and parameters alone with instructions
|
|
6
|
+
# to call the enable tool with the tool name to use and a JSON
|
|
7
|
+
# formatted parameters.
|
|
8
|
+
#
|
|
9
|
+
# This could be something lie a tool dispatcher that can call the
|
|
10
|
+
# tool with the parameters and return the result.
|
|
11
|
+
#
|
|
12
|
+
# Maybe we have the DispatcherTool and a ToolBox tool where the ToolBox Tool
|
|
13
|
+
# returns to the LLM the list of tool names, parameters and descriptions.
|
|
14
|
+
# The DispatcherTool can then call the tool with the parameters and return the result.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
require 'ruby_llm/tool'
|
|
18
|
+
|
|
19
|
+
module Tools
|
|
20
|
+
# A tool for reading the contents of a file.
|
|
21
|
+
class Enabler < RubyLLM::Tool
|
|
22
|
+
attr_reader :agent
|
|
23
|
+
|
|
24
|
+
description <<~DESCRIPTION
|
|
25
|
+
Enables a tool based on the request.
|
|
26
|
+
DESCRIPTION
|
|
27
|
+
param :tool, desc: 'Tool name to enable'
|
|
28
|
+
# param :params, desc: 'JSON formatted parameters'
|
|
29
|
+
|
|
30
|
+
def initialize(agent)
|
|
31
|
+
@agent = agent
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execute(tool:, params: '')
|
|
35
|
+
agent.add_tool(tool)
|
|
36
|
+
# TODO: call the tool with the params and return the result.
|
|
37
|
+
{ success: true }
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ error: e.message }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -104,9 +104,11 @@ module SharedTools
|
|
|
104
104
|
# @param max_retries [Integer] Maximum retry attempts
|
|
105
105
|
#
|
|
106
106
|
# @return [Hash] Operation result with success status
|
|
107
|
-
def execute(operation:, simulate_error: nil, max_retries: 3, **
|
|
107
|
+
def execute(operation:, simulate_error: nil, max_retries: 3, data: {}, **_rest)
|
|
108
108
|
@operation_start_time = Time.now
|
|
109
109
|
@logger.info("ErrorHandlingTool#execute operation=#{operation} simulate_error=#{simulate_error}")
|
|
110
|
+
# Normalise data keys to symbols (JSON tool calls use string keys)
|
|
111
|
+
data = (data || {}).transform_keys(&:to_sym)
|
|
110
112
|
|
|
111
113
|
begin
|
|
112
114
|
# Validate inputs
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedTools
|
|
4
|
+
module Tools
|
|
5
|
+
module Notification
|
|
6
|
+
# Abstract base class for platform-specific notification drivers.
|
|
7
|
+
# Subclasses must implement notify, alert, and speak.
|
|
8
|
+
class BaseDriver
|
|
9
|
+
# Show a non-blocking desktop banner notification.
|
|
10
|
+
#
|
|
11
|
+
# @param message [String]
|
|
12
|
+
# @param title [String, nil]
|
|
13
|
+
# @param subtitle [String, nil]
|
|
14
|
+
# @param sound [String, nil]
|
|
15
|
+
# @return [Hash]
|
|
16
|
+
def notify(message:, title: nil, subtitle: nil, sound: nil)
|
|
17
|
+
raise NotImplementedError, "#{self.class}##{__method__} undefined"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Show a modal dialog and wait for the user to click a button.
|
|
21
|
+
#
|
|
22
|
+
# @param message [String]
|
|
23
|
+
# @param title [String, nil]
|
|
24
|
+
# @param buttons [Array<String>]
|
|
25
|
+
# @param default_button [String, nil]
|
|
26
|
+
# @return [Hash] includes :button with the label of the clicked button
|
|
27
|
+
def alert(message:, title: nil, buttons: ['OK'], default_button: nil)
|
|
28
|
+
raise NotImplementedError, "#{self.class}##{__method__} undefined"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Speak text aloud using text-to-speech.
|
|
32
|
+
#
|
|
33
|
+
# @param text [String]
|
|
34
|
+
# @param voice [String, nil]
|
|
35
|
+
# @param rate [Integer, nil] words per minute
|
|
36
|
+
# @return [Hash]
|
|
37
|
+
def speak(text:, voice: nil, rate: nil)
|
|
38
|
+
raise NotImplementedError, "#{self.class}##{__method__} undefined"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
|
|
43
|
+
# @param cmd [String]
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def command_available?(cmd)
|
|
46
|
+
system("which #{cmd} > /dev/null 2>&1")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|