shared_tools 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +594 -42
  4. data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +20 -3
  5. data/lib/shared_tools/mcp/imcp.rb +28 -0
  6. data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
  7. data/lib/shared_tools/mcp.rb +24 -0
  8. data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
  9. data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
  10. data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
  11. data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
  12. data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
  13. data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
  14. data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
  15. data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
  16. data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
  17. data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
  18. data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
  19. data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
  20. data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
  21. data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
  22. data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
  23. data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
  24. data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
  25. data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
  26. data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
  27. data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
  28. data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
  29. data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
  30. data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
  31. data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
  32. data/lib/shared_tools/tools/browser.rb +27 -0
  33. data/lib/shared_tools/tools/browser_tool.rb +255 -0
  34. data/lib/shared_tools/tools/calculator_tool.rb +169 -0
  35. data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
  36. data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
  37. data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
  38. data/lib/shared_tools/tools/computer.rb +21 -0
  39. data/lib/shared_tools/tools/computer_tool.rb +207 -0
  40. data/lib/shared_tools/tools/data_science_kit.rb +707 -0
  41. data/lib/shared_tools/tools/database/base_driver.rb +17 -0
  42. data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
  43. data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
  44. data/lib/shared_tools/tools/database.rb +9 -0
  45. data/lib/shared_tools/tools/database_query_tool.rb +313 -0
  46. data/lib/shared_tools/tools/database_tool.rb +99 -0
  47. data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
  48. data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
  49. data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
  50. data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
  51. data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
  52. data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
  53. data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
  54. data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
  55. data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
  56. data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
  57. data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
  58. data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
  59. data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
  60. data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
  61. data/lib/shared_tools/tools/disk.rb +17 -0
  62. data/lib/shared_tools/tools/disk_tool.rb +132 -0
  63. data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
  64. data/lib/shared_tools/tools/doc.rb +8 -0
  65. data/lib/shared_tools/tools/doc_tool.rb +109 -0
  66. data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
  67. data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
  68. data/lib/shared_tools/tools/docker.rb +8 -0
  69. data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
  70. data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
  71. data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
  72. data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
  73. data/lib/shared_tools/tools/eval.rb +10 -0
  74. data/lib/shared_tools/tools/eval_tool.rb +139 -0
  75. data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
  76. data/lib/shared_tools/tools/version.rb +7 -0
  77. data/lib/shared_tools/tools/weather_tool.rb +197 -0
  78. data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
  79. data/lib/shared_tools/tools.rb +16 -0
  80. data/lib/shared_tools/version.rb +1 -1
  81. data/lib/shared_tools.rb +9 -24
  82. metadata +189 -68
  83. data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
  84. data/lib/shared_tools/llm_rb.rb +0 -9
  85. data/lib/shared_tools/omniai.rb +0 -9
  86. data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
  87. data/lib/shared_tools/raix.rb +0 -9
  88. data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
  89. data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
  90. data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
  91. data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
  92. data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
  93. data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
  94. data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
  95. data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
  96. data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
  97. data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
  98. data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
  99. data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -15
  100. data/lib/shared_tools/ruby_llm/mcp.rb +0 -12
  101. data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
  102. data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
  103. data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
  104. data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
  105. data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
  106. data/lib/shared_tools/ruby_llm.rb +0 -12
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Database
6
+ # Base class for database drivers (e.g. sqlite, postgres, mysql, etc).
7
+ class BaseDriver
8
+ # @param statement [String] e.g. "SELECT * FROM people"
9
+ #
10
+ # @return [Hash] e.g. { status: :ok, result: [["id", "name"], [1, "John"], [2, "Paul"], ...] }
11
+ def perform(statement:)
12
+ raise NotImplementedError, "#{self.class}##{__method__} undefined"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Database
6
+ # @example
7
+ # connection = PG.connect(dbname: "testdb")
8
+ # driver = SharedTools::Tools::Database::PostgresDriver.new
9
+ # driver.perform(statement: "SELECT * FROM people")
10
+ class PostgresDriver < BaseDriver
11
+ # @param connection [Sqlite3::Database]
12
+ def initialize(connection:)
13
+ super()
14
+ @connection = connection
15
+ end
16
+
17
+ # @param statement [String]
18
+ #
19
+ # @return [Hash]
20
+ def perform(statement:)
21
+ @connection.exec(statement) do |result|
22
+ { status: :ok, result: [result.fields] + result.values }
23
+ end
24
+ rescue ::PG::Error => e
25
+ { status: :error, message: e.message }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedTools
4
+ module Tools
5
+ module Database
6
+ # @example
7
+ # driver = SharedTools::Tools::Database::SqliteDriver.new
8
+ # driver.perform(statement: "SELECT * FROM people")
9
+ class SqliteDriver < BaseDriver
10
+ # @param db [Sqlite3::Database]
11
+ def initialize(db:)
12
+ super()
13
+ @db = db
14
+ end
15
+
16
+ # @param statement [String]
17
+ #
18
+ # @return [Hash]
19
+ def perform(statement:)
20
+ result = @db.execute2(statement)
21
+
22
+ { status: :ok, result: }
23
+ rescue ::SQLite3::Exception => e
24
+ { status: :error, message: e.message }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection loader for database tools
4
+ # Usage: require 'shared_tools/tools/database'
5
+
6
+ require 'shared_tools'
7
+
8
+ require_relative 'database/base_driver'
9
+ require_relative 'database_tool'
@@ -0,0 +1,313 @@
1
+ # database_query_tool.rb - Safe database query execution
2
+ require 'ruby_llm/tool'
3
+ require 'sequel'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ class DatabaseQueryTool < RubyLLM::Tool
8
+ def self.name = 'database_query'
9
+
10
+ description <<~'DESCRIPTION'
11
+ Execute safe, read-only database queries with automatic connection management and security controls.
12
+ This tool is designed for secure data retrieval operations only, restricting access to SELECT statements
13
+ to prevent any data modification. It includes automatic connection pooling, query result limiting,
14
+ query timeout support, and comprehensive error handling. The tool supports multiple database configurations
15
+ through environment variables and ensures all connections are properly closed after use.
16
+ Perfect for AI-assisted data analysis and reporting workflows where read-only access is required.
17
+
18
+ Security features:
19
+ - SELECT-only queries (no INSERT, UPDATE, DELETE, DROP, etc.)
20
+ - Automatic LIMIT clause enforcement
21
+ - Query timeout protection
22
+ - Prepared statement support to prevent SQL injection
23
+ - Connection pooling with automatic cleanup
24
+
25
+ Supported databases:
26
+ - PostgreSQL, MySQL, SQLite, SQL Server, Oracle, and any database supported by Sequel
27
+
28
+ Example usage:
29
+ tool = SharedTools::Tools::DatabaseQueryTool.new
30
+ result = tool.execute(query: "SELECT * FROM users WHERE active = ?", params: [true])
31
+ puts "Found #{result[:row_count]} users"
32
+ DESCRIPTION
33
+
34
+ params do
35
+ string :query, description: <<~DESC.strip
36
+ SQL SELECT query to execute against the database. Only SELECT statements are permitted
37
+ for security reasons - INSERT, UPDATE, DELETE, and DDL statements will be rejected.
38
+ The query should be well-formed SQL appropriate for the target database system.
39
+
40
+ Use placeholders (?) for parameterized queries to prevent SQL injection:
41
+ - Good: "SELECT * FROM users WHERE id = ?"
42
+ - Bad: "SELECT * FROM users WHERE id = \#{user_id}"
43
+
44
+ Examples:
45
+ - 'SELECT * FROM users WHERE active = true'
46
+ - 'SELECT COUNT(*) FROM orders'
47
+ - 'SELECT name, email FROM customers WHERE created_at > ?'
48
+ DESC
49
+
50
+ string :database, description: <<~DESC.strip, required: false
51
+ Database configuration name to use for the connection. This corresponds to environment
52
+ variables like DATABASE_URL, STAGING_DATABASE_URL, etc. The tool will look for
53
+ an environment variable named {DATABASE_NAME}_DATABASE_URL (uppercase).
54
+ Default is 'default' which looks for DEFAULT_DATABASE_URL or DATABASE_URL environment variable.
55
+ Common values: 'default', 'staging', 'analytics', 'reporting', 'production'.
56
+ DESC
57
+
58
+ integer :limit, description: <<~DESC.strip, required: false
59
+ Maximum number of rows to return from the query to prevent excessive memory usage
60
+ and long response times. The tool automatically adds a LIMIT clause if one is not
61
+ present in the original query. Set to a reasonable value based on expected data size.
62
+ Minimum: 1, Maximum: 10000, Default: 100. For large datasets, consider using
63
+ pagination or more specific WHERE clauses.
64
+ DESC
65
+
66
+ integer :timeout, description: <<~DESC.strip, required: false
67
+ Query timeout in seconds. If the query takes longer than this to execute, it will
68
+ be cancelled and an error will be returned. This prevents long-running queries from
69
+ consuming excessive resources. Minimum: 1, Maximum: 300, Default: 30 seconds.
70
+ DESC
71
+
72
+ array :params, of: :string, description: <<~DESC.strip, required: false
73
+ Parameters to bind to the query placeholders (?). Use parameterized queries to prevent
74
+ SQL injection vulnerabilities. The number of parameters must match the number of
75
+ placeholders in the query. Parameters are automatically escaped and quoted based on
76
+ their type. Example: params: [1, "john@example.com", true] for a query with 3 placeholders.
77
+ DESC
78
+ end
79
+
80
+ # @param logger [Logger] optional logger
81
+ def initialize(logger: nil)
82
+ @logger = logger || RubyLLM.logger
83
+ @connection_cache = {}
84
+ end
85
+
86
+ # Execute read-only database query
87
+ #
88
+ # @param query [String] SQL SELECT query to execute
89
+ # @param database [String] Database configuration name
90
+ # @param limit [Integer] Maximum rows to return (1-10000), default 100
91
+ # @param timeout [Integer] Query timeout in seconds (1-300), default 30
92
+ # @param params [Array] Parameters for parameterized query
93
+ #
94
+ # @return [Hash] Query results with success status
95
+ def execute(query:, database: "default", limit: 100, timeout: 30, params: [])
96
+ @logger.info("DatabaseQueryTool#execute database=#{database} limit=#{limit} timeout=#{timeout}")
97
+
98
+ begin
99
+ # Validate and sanitize inputs
100
+ validate_query(query)
101
+ limit = validate_limit(limit)
102
+ timeout = validate_timeout(timeout)
103
+
104
+ # Get or create database connection (cached for in-memory databases)
105
+ db = get_connection(database, timeout)
106
+
107
+ # Add LIMIT clause if not present
108
+ limited_query = add_limit_to_query(query, limit)
109
+
110
+ @logger.debug("Executing query: #{limited_query}")
111
+ @logger.debug("With parameters: #{params.inspect}") if params && !params.empty?
112
+
113
+ # Execute query with parameters
114
+ start_time = Time.now
115
+ results = if params && !params.empty?
116
+ db[limited_query, *params].all
117
+ else
118
+ db[limited_query].all
119
+ end
120
+ execution_time = Time.now - start_time
121
+
122
+ @logger.info("Query executed successfully: #{results.length} rows in #{execution_time.round(3)}s")
123
+
124
+ {
125
+ success: true,
126
+ query: limited_query,
127
+ row_count: results.length,
128
+ data: results,
129
+ database: database,
130
+ execution_time: execution_time.round(3),
131
+ executed_at: Time.now.iso8601
132
+ }
133
+ rescue Sequel::DatabaseError => e
134
+ @logger.error("Database error: #{e.message}")
135
+ {
136
+ success: false,
137
+ error: "Database error: #{e.message}",
138
+ error_type: "database_error",
139
+ query: query,
140
+ database: database
141
+ }
142
+ rescue => e
143
+ @logger.error("Query execution failed: #{e.message}")
144
+ {
145
+ success: false,
146
+ error: e.message,
147
+ error_type: e.class.name,
148
+ query: query,
149
+ database: database
150
+ }
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ # Validate that query is a SELECT statement
157
+ #
158
+ # @param query [String] SQL query to validate
159
+ # @raise [ArgumentError] if query is not a SELECT statement
160
+ def validate_query(query)
161
+ raise ArgumentError, "Query cannot be empty" if query.nil? || query.strip.empty?
162
+
163
+ # Remove comments and normalize whitespace
164
+ normalized_query = query.gsub(/--.*$/, '').gsub(/\/\*.*?\*\//m, '').strip.downcase
165
+
166
+ # Check for dangerous keywords FIRST (more specific error messages)
167
+ dangerous_keywords = %w[insert update delete drop alter truncate grant revoke]
168
+ dangerous_keywords.each do |keyword|
169
+ if normalized_query.match?(/\b#{keyword}\b/)
170
+ raise ArgumentError, "Query contains forbidden keyword: #{keyword.upcase}"
171
+ end
172
+ end
173
+
174
+ # Then check for SELECT at the start (allowing WITH clauses)
175
+ unless normalized_query.start_with?('select') || normalized_query.start_with?('with')
176
+ raise ArgumentError, "Only SELECT queries are allowed for security. Got: #{query[0..50]}"
177
+ end
178
+
179
+ @logger.debug("Query validation passed")
180
+ end
181
+
182
+ # Validate and normalize limit parameter
183
+ #
184
+ # @param limit [Integer] Requested limit
185
+ # @return [Integer] Validated limit (1-10000)
186
+ def validate_limit(limit)
187
+ limit = limit.to_i
188
+
189
+ if limit < 1
190
+ @logger.warn("Limit #{limit} is too low, adjusting to 1")
191
+ return 1
192
+ end
193
+
194
+ if limit > 10000
195
+ @logger.warn("Limit #{limit} exceeds maximum, adjusting to 10000")
196
+ return 10000
197
+ end
198
+
199
+ limit
200
+ end
201
+
202
+ # Validate and normalize timeout parameter
203
+ #
204
+ # @param timeout [Integer] Requested timeout in seconds
205
+ # @return [Integer] Validated timeout (1-300)
206
+ def validate_timeout(timeout)
207
+ timeout = timeout.to_i
208
+
209
+ if timeout < 1
210
+ @logger.warn("Timeout #{timeout} is too low, adjusting to 1")
211
+ return 1
212
+ end
213
+
214
+ if timeout > 300
215
+ @logger.warn("Timeout #{timeout} exceeds maximum, adjusting to 300")
216
+ return 300
217
+ end
218
+
219
+ timeout
220
+ end
221
+
222
+ # Get or create cached database connection
223
+ #
224
+ # @param database_name [String] Database configuration name
225
+ # @param timeout [Integer] Query timeout in seconds
226
+ # @return [Sequel::Database] Database connection
227
+ def get_connection(database_name, timeout)
228
+ connection_string = find_connection_string(database_name)
229
+
230
+ unless connection_string
231
+ error_msg = "Database connection not configured for '#{database_name}'. " \
232
+ "Please set #{database_name.upcase}_DATABASE_URL environment variable."
233
+ @logger.error(error_msg)
234
+ raise ArgumentError, error_msg
235
+ end
236
+
237
+ # Cache connections for in-memory databases to prevent data loss
238
+ # This allows multiple execute calls to share the same in-memory database
239
+ is_memory_db = connection_string.match?(/sqlite:(:|\/\/file:.*mode=memory)/)
240
+ if is_memory_db
241
+ cache_key = "#{database_name}:#{connection_string}"
242
+ @connection_cache[cache_key] ||= connect_to_database(connection_string, timeout)
243
+ else
244
+ # For regular databases, create new connection each time (don't cache)
245
+ connect_to_database(connection_string, timeout)
246
+ end
247
+ end
248
+
249
+ # Connect to database with proper error handling
250
+ #
251
+ # @param connection_string [String] Database connection string
252
+ # @param timeout [Integer] Query timeout in seconds
253
+ # @return [Sequel::Database] Database connection
254
+ def connect_to_database(connection_string, timeout)
255
+ @logger.debug("Connecting to database")
256
+
257
+ # Create connection with timeout
258
+ db = Sequel.connect(connection_string)
259
+
260
+ # Set query timeout if supported by the database
261
+ begin
262
+ case db.database_type
263
+ when :postgres
264
+ db.execute("SET statement_timeout = #{timeout * 1000}") # milliseconds
265
+ when :mysql
266
+ db.execute("SET SESSION max_execution_time = #{timeout * 1000}") # milliseconds
267
+ when :sqlite
268
+ # SQLite timeout is set at connection time via busy_timeout pragma
269
+ # The timeout parameter controls how long SQLite waits for locks
270
+ db.execute("PRAGMA busy_timeout = #{timeout * 1000}") # milliseconds
271
+ end
272
+ rescue => e
273
+ @logger.warn("Could not set query timeout: #{e.message}")
274
+ end
275
+
276
+ db
277
+ end
278
+
279
+ # Find connection string from environment variables
280
+ #
281
+ # @param database_name [String] Database configuration name
282
+ # @return [String, nil] Connection string or nil if not found
283
+ def find_connection_string(database_name)
284
+ # Try with database name prefix
285
+ connection_string = ENV["#{database_name.upcase}_DATABASE_URL"]
286
+ return connection_string if connection_string
287
+
288
+ # For 'default', also try without prefix
289
+ if database_name.downcase == 'default'
290
+ connection_string = ENV['DATABASE_URL']
291
+ return connection_string if connection_string
292
+ end
293
+
294
+ nil
295
+ end
296
+
297
+ # Add LIMIT clause to query if not present
298
+ #
299
+ # @param query [String] SQL query
300
+ # @param limit [Integer] Maximum rows to return
301
+ # @return [String] Query with LIMIT clause
302
+ def add_limit_to_query(query, limit)
303
+ # Use regex to detect existing LIMIT clause (case-insensitive, handles various formats)
304
+ return query if query.match?(/\bLIMIT\s+\d+/i)
305
+
306
+ # Add LIMIT at the end of the query
307
+ # Handle queries that might end with semicolon
308
+ query = query.sub(/;\s*\z/, '')
309
+ "#{query} LIMIT #{limit}"
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared_tools'
4
+
5
+ module SharedTools
6
+ module Tools
7
+ # @example
8
+ # db = Sqlite3::Database.new("./db.sqlite")
9
+ # driver = SharedTools::Tools::Database::SqliteDriver.new(db:)
10
+ # tool = SharedTools::Tools::DatabaseTool.new(driver:)
11
+ # tool.execute(statements: ["SELECT * FROM people"])
12
+ class DatabaseTool < ::RubyLLM::Tool
13
+ def self.name = 'database_tool'
14
+ description <<~TEXT
15
+ Executes SQL commands (INSERT / UPDATE / SELECT / etc) on a database.
16
+
17
+ Example:
18
+
19
+ STATEMENTS:
20
+
21
+ [
22
+ 'CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
23
+ 'INSERT INTO people (name) VALUES ('John')',
24
+ 'INSERT INTO people (name) VALUES ('Paul')',
25
+ 'SELECT * FROM people',
26
+ 'DROP TABLE people'
27
+ ]
28
+
29
+ RESULT:
30
+
31
+ [
32
+ {
33
+ "status": "OK",
34
+ "statement": "CREATE TABLE people (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
35
+ "result": "..."
36
+ },
37
+ {
38
+ "status": "OK",
39
+ "statement": "INSERT INTO people (name) VALUES ('John')"
40
+ "result": "..."
41
+ },
42
+ {
43
+ "status": "OK",
44
+ "statement": "INSERT INTO people (name) VALUES ('Paul')",
45
+ "result": "..."
46
+ },
47
+ {
48
+ "status": "OK",
49
+ "statement": "SELECT * FROM people",
50
+ "result": "..."
51
+ },
52
+ {
53
+ "status": "OK",
54
+ "statement": "DROP TABLE people",
55
+ "result": "..."
56
+ }
57
+ ]
58
+ TEXT
59
+
60
+ params do
61
+ array :statements, of: :string, description: "A list of SQL statements to run sequentially (e.g. ['SELECT * FROM users', 'INSERT INTO ...'])"
62
+ end
63
+
64
+
65
+ # @param driver [SharedTools::Tools::Database::BaseDriver] required database driver (SqliteDriver, PostgresDriver, etc.)
66
+ # @param logger [Logger] optional logger
67
+ def initialize(driver:, logger: nil)
68
+ raise ArgumentError, "driver is required for DatabaseTool" if driver.nil?
69
+ @driver = driver
70
+ @logger = logger || RubyLLM.logger
71
+ end
72
+
73
+ # @example
74
+ # tool = SharedTools::Tools::Database::BaseTool.new
75
+ # tool.execute(statements: ["SELECT * FROM people"])
76
+ #
77
+ # @param statements [Array<String>]
78
+ #
79
+ # @return [Array<Hash>]
80
+ def execute(statements:)
81
+ [].tap do |executions|
82
+ statements.map do |statement|
83
+ execution = perform(statement:).merge(statement:)
84
+ executions << execution
85
+ break unless execution[:status].eql?(:ok)
86
+ end
87
+ end
88
+ end
89
+
90
+ def perform(statement:)
91
+ @logger&.info("#perform statement=#{statement.inspect}")
92
+
93
+ @driver.perform(statement:).tap do |result|
94
+ @logger&.info(JSON.generate(result))
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end