rcrewai 0.1.0 → 0.2.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 +32 -0
- data/docs/api/agent.md +429 -0
- data/docs/api/task.md +494 -0
- data/docs/examples/api-integration.md +829 -0
- data/docs/examples/async-execution.md +893 -0
- data/docs/examples/code-review-crew.md +660 -0
- data/docs/examples/content-marketing-pipeline.md +681 -0
- data/docs/examples/custom-tools.md +1224 -0
- data/docs/examples/customer-support.md +717 -0
- data/docs/examples/data-analysis-team.md +677 -0
- data/docs/examples/database-operations.md +1298 -0
- data/docs/examples/ecommerce-operations.md +990 -0
- data/docs/examples/financial-analysis.md +857 -0
- data/docs/examples/hierarchical-crew.md +479 -0
- data/docs/examples/product-development.md +688 -0
- data/docs/examples/production-ready-crew.md +384 -408
- data/docs/examples/research-development.md +1225 -0
- data/docs/examples/social-media.md +1073 -0
- data/docs/examples/task-automation.md +527 -0
- data/docs/examples/tool-composition.md +1075 -0
- data/docs/examples/web-scraping.md +1201 -0
- data/docs/tutorials/advanced-agents.md +1014 -0
- data/docs/tutorials/custom-tools.md +1242 -0
- data/docs/tutorials/deployment.md +1836 -0
- data/docs/tutorials/index.md +184 -0
- data/docs/tutorials/multiple-crews.md +1692 -0
- data/lib/rcrewai/llm_clients/anthropic.rb +1 -1
- data/lib/rcrewai/version.rb +1 -1
- data/rcrewai.gemspec +21 -2
- metadata +47 -5
@@ -0,0 +1,1242 @@
|
|
1
|
+
---
|
2
|
+
layout: tutorial
|
3
|
+
title: Custom Tools Development
|
4
|
+
description: Learn how to build custom tools to extend agent capabilities for specialized tasks
|
5
|
+
---
|
6
|
+
|
7
|
+
# Custom Tools Development
|
8
|
+
|
9
|
+
This tutorial teaches you how to create custom tools that extend agent capabilities beyond the built-in tools. You'll learn tool architecture, implementation patterns, testing strategies, and best practices.
|
10
|
+
|
11
|
+
## Table of Contents
|
12
|
+
1. [Understanding Tool Architecture](#understanding-tool-architecture)
|
13
|
+
2. [Creating Basic Custom Tools](#creating-basic-custom-tools)
|
14
|
+
3. [Advanced Tool Features](#advanced-tool-features)
|
15
|
+
4. [API Integration Tools](#api-integration-tools)
|
16
|
+
5. [Database Tools](#database-tools)
|
17
|
+
6. [File Processing Tools](#file-processing-tools)
|
18
|
+
7. [Testing Custom Tools](#testing-custom-tools)
|
19
|
+
8. [Tool Security and Validation](#tool-security-and-validation)
|
20
|
+
|
21
|
+
## Understanding Tool Architecture
|
22
|
+
|
23
|
+
### Tool Base Class
|
24
|
+
|
25
|
+
All tools in RCrewAI inherit from the base Tool class:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
module RCrewAI
|
29
|
+
module Tools
|
30
|
+
class Base
|
31
|
+
attr_reader :name, :description
|
32
|
+
|
33
|
+
def initialize(**options)
|
34
|
+
@name = self.class.name.split('::').last.downcase
|
35
|
+
@description = "Base tool description"
|
36
|
+
@options = options
|
37
|
+
@logger = Logger.new($stdout)
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute(**params)
|
41
|
+
raise NotImplementedError, "Subclasses must implement execute method"
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_params!(params, required: [], optional: [])
|
45
|
+
# Built-in parameter validation
|
46
|
+
required.each do |param|
|
47
|
+
unless params.key?(param)
|
48
|
+
raise ToolError, "Missing required parameter: #{param}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check for unknown parameters
|
53
|
+
all_params = required + optional
|
54
|
+
params.keys.each do |key|
|
55
|
+
unless all_params.include?(key)
|
56
|
+
raise ToolError, "Unknown parameter: #{key}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### Tool Lifecycle
|
66
|
+
|
67
|
+
1. **Initialization**: Tool is created with configuration
|
68
|
+
2. **Validation**: Parameters are validated before execution
|
69
|
+
3. **Execution**: Tool performs its function
|
70
|
+
4. **Result Formatting**: Output is formatted for agent consumption
|
71
|
+
5. **Error Handling**: Exceptions are caught and handled
|
72
|
+
|
73
|
+
## Creating Basic Custom Tools
|
74
|
+
|
75
|
+
### Simple Calculator Tool
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class CalculatorTool < RCrewAI::Tools::Base
|
79
|
+
def initialize(**options)
|
80
|
+
super
|
81
|
+
@name = 'calculator'
|
82
|
+
@description = 'Performs mathematical calculations'
|
83
|
+
@precision = options[:precision] || 2
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute(**params)
|
87
|
+
validate_params!(params, required: [:operation, :operands])
|
88
|
+
|
89
|
+
operation = params[:operation].to_s.downcase
|
90
|
+
operands = params[:operands]
|
91
|
+
|
92
|
+
# Validate operands
|
93
|
+
unless operands.is_a?(Array) && operands.all? { |o| o.is_a?(Numeric) }
|
94
|
+
raise ToolError, "Operands must be an array of numbers"
|
95
|
+
end
|
96
|
+
|
97
|
+
result = case operation
|
98
|
+
when 'add', '+'
|
99
|
+
operands.sum
|
100
|
+
when 'subtract', '-'
|
101
|
+
operands.reduce(&:-)
|
102
|
+
when 'multiply', '*'
|
103
|
+
operands.reduce(&:*)
|
104
|
+
when 'divide', '/'
|
105
|
+
divide_with_check(operands)
|
106
|
+
when 'power', '^'
|
107
|
+
operands[0] ** operands[1]
|
108
|
+
when 'sqrt'
|
109
|
+
Math.sqrt(operands[0])
|
110
|
+
when 'average'
|
111
|
+
operands.sum.to_f / operands.length
|
112
|
+
else
|
113
|
+
raise ToolError, "Unknown operation: #{operation}"
|
114
|
+
end
|
115
|
+
|
116
|
+
format_result(result)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def divide_with_check(operands)
|
122
|
+
if operands[1..-1].any? { |o| o == 0 }
|
123
|
+
raise ToolError, "Division by zero"
|
124
|
+
end
|
125
|
+
operands.reduce(&:/)
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_result(result)
|
129
|
+
if result.is_a?(Float)
|
130
|
+
"Result: #{result.round(@precision)}"
|
131
|
+
else
|
132
|
+
"Result: #{result}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Usage with an agent
|
138
|
+
calculator = CalculatorTool.new(precision: 4)
|
139
|
+
|
140
|
+
agent = RCrewAI::Agent.new(
|
141
|
+
name: "math_agent",
|
142
|
+
role: "Mathematics Specialist",
|
143
|
+
goal: "Solve mathematical problems",
|
144
|
+
tools: [calculator]
|
145
|
+
)
|
146
|
+
|
147
|
+
# Agent can now use: USE_TOOL[calculator](operation=multiply, operands=[5, 3, 2])
|
148
|
+
```
|
149
|
+
|
150
|
+
### Weather Information Tool
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
require 'net/http'
|
154
|
+
require 'json'
|
155
|
+
|
156
|
+
class WeatherTool < RCrewAI::Tools::Base
|
157
|
+
def initialize(**options)
|
158
|
+
super
|
159
|
+
@name = 'weather'
|
160
|
+
@description = 'Get current weather information for any location'
|
161
|
+
@api_key = options[:api_key] || ENV['WEATHER_API_KEY']
|
162
|
+
@base_url = 'https://api.openweathermap.org/data/2.5'
|
163
|
+
@units = options[:units] || 'metric' # metric, imperial, kelvin
|
164
|
+
end
|
165
|
+
|
166
|
+
def execute(**params)
|
167
|
+
validate_params!(params, required: [:location], optional: [:forecast])
|
168
|
+
|
169
|
+
location = params[:location]
|
170
|
+
forecast = params[:forecast] || false
|
171
|
+
|
172
|
+
begin
|
173
|
+
if forecast
|
174
|
+
get_forecast(location)
|
175
|
+
else
|
176
|
+
get_current_weather(location)
|
177
|
+
end
|
178
|
+
rescue => e
|
179
|
+
"Weather service error: #{e.message}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def get_current_weather(location)
|
186
|
+
endpoint = "#{@base_url}/weather"
|
187
|
+
params = {
|
188
|
+
q: location,
|
189
|
+
appid: @api_key,
|
190
|
+
units: @units
|
191
|
+
}
|
192
|
+
|
193
|
+
response = make_api_request(endpoint, params)
|
194
|
+
format_weather_response(response)
|
195
|
+
end
|
196
|
+
|
197
|
+
def get_forecast(location)
|
198
|
+
endpoint = "#{@base_url}/forecast"
|
199
|
+
params = {
|
200
|
+
q: location,
|
201
|
+
appid: @api_key,
|
202
|
+
units: @units,
|
203
|
+
cnt: 5 # 5 day forecast
|
204
|
+
}
|
205
|
+
|
206
|
+
response = make_api_request(endpoint, params)
|
207
|
+
format_forecast_response(response)
|
208
|
+
end
|
209
|
+
|
210
|
+
def make_api_request(endpoint, params)
|
211
|
+
uri = URI(endpoint)
|
212
|
+
uri.query = URI.encode_www_form(params)
|
213
|
+
|
214
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
215
|
+
http.use_ssl = true
|
216
|
+
http.read_timeout = 10
|
217
|
+
|
218
|
+
request = Net::HTTP::Get.new(uri)
|
219
|
+
response = http.request(request)
|
220
|
+
|
221
|
+
if response.code == '200'
|
222
|
+
JSON.parse(response.body)
|
223
|
+
else
|
224
|
+
raise "API error: #{response.code} - #{response.body}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def format_weather_response(data)
|
229
|
+
temp = data['main']['temp']
|
230
|
+
feels_like = data['main']['feels_like']
|
231
|
+
description = data['weather'][0]['description']
|
232
|
+
humidity = data['main']['humidity']
|
233
|
+
wind_speed = data['wind']['speed']
|
234
|
+
|
235
|
+
unit_label = @units == 'metric' ? '°C' : '°F'
|
236
|
+
|
237
|
+
<<~WEATHER
|
238
|
+
Weather in #{data['name']}, #{data['sys']['country']}:
|
239
|
+
Temperature: #{temp}#{unit_label} (feels like #{feels_like}#{unit_label})
|
240
|
+
Conditions: #{description}
|
241
|
+
Humidity: #{humidity}%
|
242
|
+
Wind Speed: #{wind_speed} #{@units == 'metric' ? 'm/s' : 'mph'}
|
243
|
+
WEATHER
|
244
|
+
end
|
245
|
+
|
246
|
+
def format_forecast_response(data)
|
247
|
+
forecasts = data['list'].map do |item|
|
248
|
+
time = Time.at(item['dt']).strftime('%Y-%m-%d %H:%M')
|
249
|
+
temp = item['main']['temp']
|
250
|
+
desc = item['weather'][0]['description']
|
251
|
+
"#{time}: #{temp}°, #{desc}"
|
252
|
+
end
|
253
|
+
|
254
|
+
"5-Day Forecast for #{data['city']['name']}:\n" + forecasts.join("\n")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
## Advanced Tool Features
|
260
|
+
|
261
|
+
### Tool with State Management
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
class SessionMemoryTool < RCrewAI::Tools::Base
|
265
|
+
def initialize(**options)
|
266
|
+
super
|
267
|
+
@name = 'session_memory'
|
268
|
+
@description = 'Store and retrieve information during agent session'
|
269
|
+
@memory_store = {}
|
270
|
+
@max_entries = options[:max_entries] || 100
|
271
|
+
@ttl = options[:ttl] || 3600 # 1 hour default
|
272
|
+
end
|
273
|
+
|
274
|
+
def execute(**params)
|
275
|
+
validate_params!(params,
|
276
|
+
required: [:action],
|
277
|
+
optional: [:key, :value, :pattern]
|
278
|
+
)
|
279
|
+
|
280
|
+
action = params[:action].to_sym
|
281
|
+
|
282
|
+
case action
|
283
|
+
when :store
|
284
|
+
store_value(params[:key], params[:value])
|
285
|
+
when :retrieve
|
286
|
+
retrieve_value(params[:key])
|
287
|
+
when :delete
|
288
|
+
delete_value(params[:key])
|
289
|
+
when :list
|
290
|
+
list_keys(params[:pattern])
|
291
|
+
when :clear
|
292
|
+
clear_all
|
293
|
+
else
|
294
|
+
raise ToolError, "Unknown action: #{action}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
def store_value(key, value)
|
301
|
+
raise ToolError, "Key and value required for store action" unless key && value
|
302
|
+
|
303
|
+
# Enforce size limit
|
304
|
+
if @memory_store.size >= @max_entries && !@memory_store.key?(key)
|
305
|
+
evict_oldest
|
306
|
+
end
|
307
|
+
|
308
|
+
@memory_store[key] = {
|
309
|
+
value: value,
|
310
|
+
timestamp: Time.now,
|
311
|
+
access_count: 0
|
312
|
+
}
|
313
|
+
|
314
|
+
"Stored: #{key} = #{value}"
|
315
|
+
end
|
316
|
+
|
317
|
+
def retrieve_value(key)
|
318
|
+
raise ToolError, "Key required for retrieve action" unless key
|
319
|
+
|
320
|
+
if entry = @memory_store[key]
|
321
|
+
# Check TTL
|
322
|
+
if Time.now - entry[:timestamp] > @ttl
|
323
|
+
@memory_store.delete(key)
|
324
|
+
return "Key expired: #{key}"
|
325
|
+
end
|
326
|
+
|
327
|
+
entry[:access_count] += 1
|
328
|
+
entry[:last_accessed] = Time.now
|
329
|
+
|
330
|
+
"Retrieved: #{key} = #{entry[:value]} (accessed #{entry[:access_count]} times)"
|
331
|
+
else
|
332
|
+
"Key not found: #{key}"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def delete_value(key)
|
337
|
+
if @memory_store.delete(key)
|
338
|
+
"Deleted: #{key}"
|
339
|
+
else
|
340
|
+
"Key not found: #{key}"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def list_keys(pattern = nil)
|
345
|
+
keys = @memory_store.keys
|
346
|
+
|
347
|
+
if pattern
|
348
|
+
regex = Regexp.new(pattern)
|
349
|
+
keys = keys.select { |k| k.match?(regex) }
|
350
|
+
end
|
351
|
+
|
352
|
+
"Keys (#{keys.length}): #{keys.join(', ')}"
|
353
|
+
end
|
354
|
+
|
355
|
+
def clear_all
|
356
|
+
count = @memory_store.size
|
357
|
+
@memory_store.clear
|
358
|
+
"Cleared #{count} entries"
|
359
|
+
end
|
360
|
+
|
361
|
+
def evict_oldest
|
362
|
+
oldest = @memory_store.min_by { |k, v| v[:last_accessed] || v[:timestamp] }
|
363
|
+
@memory_store.delete(oldest[0]) if oldest
|
364
|
+
end
|
365
|
+
end
|
366
|
+
```
|
367
|
+
|
368
|
+
### Async Tool with Callbacks
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
class AsyncProcessingTool < RCrewAI::Tools::Base
|
372
|
+
def initialize(**options)
|
373
|
+
super
|
374
|
+
@name = 'async_processor'
|
375
|
+
@description = 'Process tasks asynchronously with progress tracking'
|
376
|
+
@callback = options[:callback]
|
377
|
+
@thread_pool = []
|
378
|
+
@max_threads = options[:max_threads] || 5
|
379
|
+
end
|
380
|
+
|
381
|
+
def execute(**params)
|
382
|
+
validate_params!(params,
|
383
|
+
required: [:task_type, :data],
|
384
|
+
optional: [:priority, :timeout]
|
385
|
+
)
|
386
|
+
|
387
|
+
task_id = SecureRandom.uuid
|
388
|
+
priority = params[:priority] || :normal
|
389
|
+
timeout = params[:timeout] || 60
|
390
|
+
|
391
|
+
# Start async processing
|
392
|
+
thread = Thread.new do
|
393
|
+
begin
|
394
|
+
process_async(task_id, params[:task_type], params[:data], timeout)
|
395
|
+
rescue => e
|
396
|
+
handle_async_error(task_id, e)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Manage thread pool
|
401
|
+
@thread_pool << thread
|
402
|
+
cleanup_threads
|
403
|
+
|
404
|
+
"Task queued: #{task_id} (priority: #{priority})"
|
405
|
+
end
|
406
|
+
|
407
|
+
def get_status(task_id)
|
408
|
+
# Check task status
|
409
|
+
if result = check_result(task_id)
|
410
|
+
"Task #{task_id}: #{result[:status]} - #{result[:message]}"
|
411
|
+
else
|
412
|
+
"Task #{task_id}: Unknown or not started"
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
def process_async(task_id, task_type, data, timeout)
|
419
|
+
update_status(task_id, :processing, "Started at #{Time.now}")
|
420
|
+
|
421
|
+
result = Timeout::timeout(timeout) do
|
422
|
+
case task_type
|
423
|
+
when 'analysis'
|
424
|
+
perform_analysis(data)
|
425
|
+
when 'transformation'
|
426
|
+
perform_transformation(data)
|
427
|
+
when 'validation'
|
428
|
+
perform_validation(data)
|
429
|
+
else
|
430
|
+
raise "Unknown task type: #{task_type}"
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
update_status(task_id, :completed, result)
|
435
|
+
|
436
|
+
# Execute callback if provided
|
437
|
+
@callback.call(task_id, result) if @callback
|
438
|
+
|
439
|
+
rescue Timeout::Error
|
440
|
+
update_status(task_id, :timeout, "Task exceeded #{timeout}s limit")
|
441
|
+
end
|
442
|
+
|
443
|
+
def cleanup_threads
|
444
|
+
@thread_pool.reject!(&:alive?)
|
445
|
+
|
446
|
+
# Limit thread pool size
|
447
|
+
while @thread_pool.size > @max_threads
|
448
|
+
oldest = @thread_pool.shift
|
449
|
+
oldest.join(1) # Wait 1 second then continue
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def update_status(task_id, status, message)
|
454
|
+
@status_store ||= {}
|
455
|
+
@status_store[task_id] = {
|
456
|
+
status: status,
|
457
|
+
message: message,
|
458
|
+
timestamp: Time.now
|
459
|
+
}
|
460
|
+
end
|
461
|
+
|
462
|
+
def check_result(task_id)
|
463
|
+
@status_store&.[](task_id)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
```
|
467
|
+
|
468
|
+
## API Integration Tools
|
469
|
+
|
470
|
+
### REST API Client Tool
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
require 'faraday'
|
474
|
+
require 'json'
|
475
|
+
|
476
|
+
class RestApiTool < RCrewAI::Tools::Base
|
477
|
+
def initialize(**options)
|
478
|
+
super
|
479
|
+
@name = 'rest_api'
|
480
|
+
@description = 'Make REST API calls with authentication and error handling'
|
481
|
+
@base_url = options[:base_url]
|
482
|
+
@api_key = options[:api_key]
|
483
|
+
@auth_type = options[:auth_type] || :header # :header, :query, :basic
|
484
|
+
@timeout = options[:timeout] || 30
|
485
|
+
|
486
|
+
setup_client
|
487
|
+
end
|
488
|
+
|
489
|
+
def execute(**params)
|
490
|
+
validate_params!(params,
|
491
|
+
required: [:method, :endpoint],
|
492
|
+
optional: [:data, :headers, :query]
|
493
|
+
)
|
494
|
+
|
495
|
+
method = params[:method].to_s.downcase.to_sym
|
496
|
+
endpoint = params[:endpoint]
|
497
|
+
data = params[:data]
|
498
|
+
headers = params[:headers] || {}
|
499
|
+
query = params[:query] || {}
|
500
|
+
|
501
|
+
# Add authentication
|
502
|
+
headers, query = add_authentication(headers, query)
|
503
|
+
|
504
|
+
# Make request
|
505
|
+
response = make_request(method, endpoint, data, headers, query)
|
506
|
+
|
507
|
+
# Format response
|
508
|
+
format_api_response(response)
|
509
|
+
rescue Faraday::Error => e
|
510
|
+
handle_api_error(e)
|
511
|
+
end
|
512
|
+
|
513
|
+
private
|
514
|
+
|
515
|
+
def setup_client
|
516
|
+
@client = Faraday.new(url: @base_url) do |f|
|
517
|
+
f.request :json
|
518
|
+
f.response :json
|
519
|
+
f.adapter Faraday.default_adapter
|
520
|
+
f.options.timeout = @timeout
|
521
|
+
|
522
|
+
# Add middleware for logging
|
523
|
+
f.response :logger if @options[:debug]
|
524
|
+
|
525
|
+
# Add retry logic
|
526
|
+
f.request :retry, {
|
527
|
+
max: 3,
|
528
|
+
interval: 0.5,
|
529
|
+
interval_randomness: 0.5,
|
530
|
+
backoff_factor: 2
|
531
|
+
}
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def add_authentication(headers, query)
|
536
|
+
case @auth_type
|
537
|
+
when :header
|
538
|
+
headers['Authorization'] = "Bearer #{@api_key}" if @api_key
|
539
|
+
when :query
|
540
|
+
query['api_key'] = @api_key if @api_key
|
541
|
+
when :basic
|
542
|
+
headers['Authorization'] = "Basic #{Base64.encode64(@api_key)}" if @api_key
|
543
|
+
end
|
544
|
+
|
545
|
+
[headers, query]
|
546
|
+
end
|
547
|
+
|
548
|
+
def make_request(method, endpoint, data, headers, query)
|
549
|
+
case method
|
550
|
+
when :get
|
551
|
+
@client.get(endpoint, query, headers)
|
552
|
+
when :post
|
553
|
+
@client.post(endpoint, data, headers) do |req|
|
554
|
+
req.params = query
|
555
|
+
end
|
556
|
+
when :put
|
557
|
+
@client.put(endpoint, data, headers) do |req|
|
558
|
+
req.params = query
|
559
|
+
end
|
560
|
+
when :patch
|
561
|
+
@client.patch(endpoint, data, headers) do |req|
|
562
|
+
req.params = query
|
563
|
+
end
|
564
|
+
when :delete
|
565
|
+
@client.delete(endpoint, query, headers)
|
566
|
+
else
|
567
|
+
raise ToolError, "Unsupported HTTP method: #{method}"
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def format_api_response(response)
|
572
|
+
status = response.status
|
573
|
+
body = response.body
|
574
|
+
|
575
|
+
if status >= 200 && status < 300
|
576
|
+
if body.is_a?(Hash) || body.is_a?(Array)
|
577
|
+
JSON.pretty_generate(body)
|
578
|
+
else
|
579
|
+
body.to_s
|
580
|
+
end
|
581
|
+
else
|
582
|
+
"API Error (#{status}): #{body}"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def handle_api_error(error)
|
587
|
+
case error
|
588
|
+
when Faraday::TimeoutError
|
589
|
+
"API request timed out after #{@timeout} seconds"
|
590
|
+
when Faraday::ConnectionFailed
|
591
|
+
"Failed to connect to API: #{error.message}"
|
592
|
+
else
|
593
|
+
"API error: #{error.class} - #{error.message}"
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# Usage example
|
599
|
+
github_api = RestApiTool.new(
|
600
|
+
base_url: 'https://api.github.com',
|
601
|
+
api_key: ENV['GITHUB_TOKEN'],
|
602
|
+
auth_type: :header
|
603
|
+
)
|
604
|
+
|
605
|
+
# Agent can use: USE_TOOL[rest_api](method=get, endpoint=/user/repos, query={per_page: 10})
|
606
|
+
```
|
607
|
+
|
608
|
+
### GraphQL Client Tool
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
class GraphQLTool < RCrewAI::Tools::Base
|
612
|
+
def initialize(**options)
|
613
|
+
super
|
614
|
+
@name = 'graphql'
|
615
|
+
@description = 'Execute GraphQL queries and mutations'
|
616
|
+
@endpoint = options[:endpoint]
|
617
|
+
@api_key = options[:api_key]
|
618
|
+
@client = setup_graphql_client
|
619
|
+
end
|
620
|
+
|
621
|
+
def execute(**params)
|
622
|
+
validate_params!(params,
|
623
|
+
required: [:query],
|
624
|
+
optional: [:variables, :operation_name]
|
625
|
+
)
|
626
|
+
|
627
|
+
query = params[:query]
|
628
|
+
variables = params[:variables] || {}
|
629
|
+
operation_name = params[:operation_name]
|
630
|
+
|
631
|
+
response = @client.execute(
|
632
|
+
query,
|
633
|
+
variables: variables,
|
634
|
+
operation_name: operation_name
|
635
|
+
)
|
636
|
+
|
637
|
+
format_graphql_response(response)
|
638
|
+
rescue => e
|
639
|
+
"GraphQL error: #{e.message}"
|
640
|
+
end
|
641
|
+
|
642
|
+
private
|
643
|
+
|
644
|
+
def setup_graphql_client
|
645
|
+
Faraday.new(url: @endpoint) do |f|
|
646
|
+
f.request :json
|
647
|
+
f.response :json
|
648
|
+
f.adapter Faraday.default_adapter
|
649
|
+
|
650
|
+
# Add authentication
|
651
|
+
f.headers['Authorization'] = "Bearer #{@api_key}" if @api_key
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
def format_graphql_response(response)
|
656
|
+
if response['errors']
|
657
|
+
errors = response['errors'].map { |e| e['message'] }.join(', ')
|
658
|
+
"GraphQL errors: #{errors}"
|
659
|
+
elsif response['data']
|
660
|
+
JSON.pretty_generate(response['data'])
|
661
|
+
else
|
662
|
+
"Empty response"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
```
|
667
|
+
|
668
|
+
## Database Tools
|
669
|
+
|
670
|
+
### SQL Database Tool
|
671
|
+
|
672
|
+
```ruby
|
673
|
+
require 'sequel'
|
674
|
+
|
675
|
+
class SqlDatabaseTool < RCrewAI::Tools::Base
|
676
|
+
def initialize(**options)
|
677
|
+
super
|
678
|
+
@name = 'sql_database'
|
679
|
+
@description = 'Execute SQL queries with safety checks'
|
680
|
+
@connection_string = options[:connection_string]
|
681
|
+
@read_only = options[:read_only] || true
|
682
|
+
@max_rows = options[:max_rows] || 100
|
683
|
+
@timeout = options[:timeout] || 30
|
684
|
+
|
685
|
+
setup_connection
|
686
|
+
end
|
687
|
+
|
688
|
+
def execute(**params)
|
689
|
+
validate_params!(params, required: [:query], optional: [:params])
|
690
|
+
|
691
|
+
query = params[:query]
|
692
|
+
query_params = params[:params] || []
|
693
|
+
|
694
|
+
# Safety checks
|
695
|
+
validate_query_safety(query) if @read_only
|
696
|
+
|
697
|
+
# Execute query
|
698
|
+
result = execute_query(query, query_params)
|
699
|
+
|
700
|
+
# Format result
|
701
|
+
format_query_result(result)
|
702
|
+
rescue => e
|
703
|
+
"Database error: #{e.message}"
|
704
|
+
end
|
705
|
+
|
706
|
+
private
|
707
|
+
|
708
|
+
def setup_connection
|
709
|
+
@db = Sequel.connect(@connection_string)
|
710
|
+
@db.extension :pg_json if @connection_string.include?('postgres')
|
711
|
+
|
712
|
+
# Set connection options
|
713
|
+
@db.pool.connection_validation_timeout = -1
|
714
|
+
@db.pool.max_connections = 5
|
715
|
+
end
|
716
|
+
|
717
|
+
def validate_query_safety(query)
|
718
|
+
unsafe_keywords = %w[
|
719
|
+
INSERT UPDATE DELETE DROP CREATE ALTER TRUNCATE
|
720
|
+
EXEC EXECUTE GRANT REVOKE
|
721
|
+
]
|
722
|
+
|
723
|
+
query_upper = query.upcase
|
724
|
+
unsafe_keywords.each do |keyword|
|
725
|
+
if query_upper.include?(keyword)
|
726
|
+
raise ToolError, "Unsafe operation '#{keyword}' not allowed in read-only mode"
|
727
|
+
end
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
def execute_query(query, params)
|
732
|
+
Timeout::timeout(@timeout) do
|
733
|
+
dataset = @db[query, *params]
|
734
|
+
|
735
|
+
# Limit results
|
736
|
+
dataset = dataset.limit(@max_rows) if dataset.respond_to?(:limit)
|
737
|
+
|
738
|
+
# Execute and fetch
|
739
|
+
if query.upcase.start_with?('SELECT')
|
740
|
+
dataset.all
|
741
|
+
else
|
742
|
+
rows_affected = dataset
|
743
|
+
{ rows_affected: rows_affected }
|
744
|
+
end
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
def format_query_result(result)
|
749
|
+
if result.is_a?(Array)
|
750
|
+
# Format as table
|
751
|
+
return "No results" if result.empty?
|
752
|
+
|
753
|
+
headers = result.first.keys
|
754
|
+
rows = result.map { |r| r.values }
|
755
|
+
|
756
|
+
format_table(headers, rows)
|
757
|
+
elsif result.is_a?(Hash)
|
758
|
+
"Query executed: #{result[:rows_affected]} rows affected"
|
759
|
+
else
|
760
|
+
result.to_s
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
def format_table(headers, rows)
|
765
|
+
# Calculate column widths
|
766
|
+
widths = headers.map(&:to_s).map(&:length)
|
767
|
+
rows.each do |row|
|
768
|
+
row.each_with_index do |cell, i|
|
769
|
+
widths[i] = [widths[i], cell.to_s.length].max
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
773
|
+
# Build table
|
774
|
+
separator = "+" + widths.map { |w| "-" * (w + 2) }.join("+") + "+"
|
775
|
+
header_row = "|" + headers.each_with_index.map { |h, i|
|
776
|
+
" #{h.to_s.ljust(widths[i])} "
|
777
|
+
}.join("|") + "|"
|
778
|
+
|
779
|
+
table = [separator, header_row, separator]
|
780
|
+
|
781
|
+
rows.each do |row|
|
782
|
+
row_str = "|" + row.each_with_index.map { |cell, i|
|
783
|
+
" #{cell.to_s.ljust(widths[i])} "
|
784
|
+
}.join("|") + "|"
|
785
|
+
table << row_str
|
786
|
+
end
|
787
|
+
|
788
|
+
table << separator
|
789
|
+
table.join("\n")
|
790
|
+
end
|
791
|
+
end
|
792
|
+
```
|
793
|
+
|
794
|
+
## File Processing Tools
|
795
|
+
|
796
|
+
### Document Processor Tool
|
797
|
+
|
798
|
+
```ruby
|
799
|
+
require 'pdf-reader'
|
800
|
+
require 'docx'
|
801
|
+
require 'csv'
|
802
|
+
|
803
|
+
class DocumentProcessorTool < RCrewAI::Tools::Base
|
804
|
+
SUPPORTED_FORMATS = %w[.pdf .docx .txt .csv .json]
|
805
|
+
|
806
|
+
def initialize(**options)
|
807
|
+
super
|
808
|
+
@name = 'document_processor'
|
809
|
+
@description = 'Extract and process content from various document formats'
|
810
|
+
@max_file_size = options[:max_file_size] || 10_000_000 # 10MB
|
811
|
+
end
|
812
|
+
|
813
|
+
def execute(**params)
|
814
|
+
validate_params!(params,
|
815
|
+
required: [:file_path],
|
816
|
+
optional: [:operation, :options]
|
817
|
+
)
|
818
|
+
|
819
|
+
file_path = params[:file_path]
|
820
|
+
operation = params[:operation] || :extract_text
|
821
|
+
options = params[:options] || {}
|
822
|
+
|
823
|
+
# Validate file
|
824
|
+
validate_file(file_path)
|
825
|
+
|
826
|
+
# Process based on operation
|
827
|
+
case operation.to_sym
|
828
|
+
when :extract_text
|
829
|
+
extract_text(file_path, options)
|
830
|
+
when :extract_metadata
|
831
|
+
extract_metadata(file_path)
|
832
|
+
when :convert
|
833
|
+
convert_document(file_path, options)
|
834
|
+
when :analyze
|
835
|
+
analyze_document(file_path, options)
|
836
|
+
else
|
837
|
+
raise ToolError, "Unknown operation: #{operation}"
|
838
|
+
end
|
839
|
+
end
|
840
|
+
|
841
|
+
private
|
842
|
+
|
843
|
+
def validate_file(file_path)
|
844
|
+
unless File.exist?(file_path)
|
845
|
+
raise ToolError, "File not found: #{file_path}"
|
846
|
+
end
|
847
|
+
|
848
|
+
if File.size(file_path) > @max_file_size
|
849
|
+
raise ToolError, "File too large: #{File.size(file_path)} bytes"
|
850
|
+
end
|
851
|
+
|
852
|
+
ext = File.extname(file_path).downcase
|
853
|
+
unless SUPPORTED_FORMATS.include?(ext)
|
854
|
+
raise ToolError, "Unsupported format: #{ext}"
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
def extract_text(file_path, options)
|
859
|
+
ext = File.extname(file_path).downcase
|
860
|
+
|
861
|
+
text = case ext
|
862
|
+
when '.pdf'
|
863
|
+
extract_pdf_text(file_path, options)
|
864
|
+
when '.docx'
|
865
|
+
extract_docx_text(file_path)
|
866
|
+
when '.txt'
|
867
|
+
File.read(file_path)
|
868
|
+
when '.csv'
|
869
|
+
extract_csv_text(file_path, options)
|
870
|
+
when '.json'
|
871
|
+
JSON.pretty_generate(JSON.parse(File.read(file_path)))
|
872
|
+
end
|
873
|
+
|
874
|
+
# Apply options
|
875
|
+
if options[:max_length]
|
876
|
+
text = text[0...options[:max_length]]
|
877
|
+
end
|
878
|
+
|
879
|
+
if options[:clean]
|
880
|
+
text = clean_text(text)
|
881
|
+
end
|
882
|
+
|
883
|
+
text
|
884
|
+
end
|
885
|
+
|
886
|
+
def extract_pdf_text(file_path, options)
|
887
|
+
reader = PDF::Reader.new(file_path)
|
888
|
+
|
889
|
+
if options[:page]
|
890
|
+
# Extract specific page
|
891
|
+
page = reader.pages[options[:page] - 1]
|
892
|
+
page&.text || ""
|
893
|
+
else
|
894
|
+
# Extract all pages
|
895
|
+
reader.pages.map(&:text).join("\n\n")
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
def extract_docx_text(file_path)
|
900
|
+
doc = Docx::Document.open(file_path)
|
901
|
+
doc.paragraphs.map(&:text).join("\n")
|
902
|
+
end
|
903
|
+
|
904
|
+
def extract_csv_text(file_path, options)
|
905
|
+
csv_options = {
|
906
|
+
headers: options[:headers] != false,
|
907
|
+
encoding: options[:encoding] || 'UTF-8'
|
908
|
+
}
|
909
|
+
|
910
|
+
rows = CSV.read(file_path, csv_options)
|
911
|
+
|
912
|
+
if options[:as_json]
|
913
|
+
JSON.pretty_generate(rows.map(&:to_h))
|
914
|
+
else
|
915
|
+
CSV.generate do |csv|
|
916
|
+
rows.each { |row| csv << row }
|
917
|
+
end
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
def extract_metadata(file_path)
|
922
|
+
metadata = {
|
923
|
+
filename: File.basename(file_path),
|
924
|
+
size: File.size(file_path),
|
925
|
+
modified: File.mtime(file_path),
|
926
|
+
format: File.extname(file_path)
|
927
|
+
}
|
928
|
+
|
929
|
+
ext = File.extname(file_path).downcase
|
930
|
+
|
931
|
+
case ext
|
932
|
+
when '.pdf'
|
933
|
+
reader = PDF::Reader.new(file_path)
|
934
|
+
metadata[:pages] = reader.page_count
|
935
|
+
metadata[:info] = reader.info
|
936
|
+
when '.docx'
|
937
|
+
doc = Docx::Document.open(file_path)
|
938
|
+
metadata[:paragraphs] = doc.paragraphs.count
|
939
|
+
metadata[:tables] = doc.tables.count
|
940
|
+
end
|
941
|
+
|
942
|
+
JSON.pretty_generate(metadata)
|
943
|
+
end
|
944
|
+
|
945
|
+
def analyze_document(file_path, options)
|
946
|
+
text = extract_text(file_path, {})
|
947
|
+
|
948
|
+
analysis = {
|
949
|
+
character_count: text.length,
|
950
|
+
word_count: text.split.length,
|
951
|
+
line_count: text.lines.count,
|
952
|
+
paragraph_count: text.split(/\n\n+/).length
|
953
|
+
}
|
954
|
+
|
955
|
+
if options[:keywords]
|
956
|
+
keywords = options[:keywords]
|
957
|
+
analysis[:keyword_frequency] = {}
|
958
|
+
|
959
|
+
keywords.each do |keyword|
|
960
|
+
count = text.scan(/#{Regexp.escape(keyword)}/i).length
|
961
|
+
analysis[:keyword_frequency][keyword] = count
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
965
|
+
JSON.pretty_generate(analysis)
|
966
|
+
end
|
967
|
+
|
968
|
+
def clean_text(text)
|
969
|
+
# Remove extra whitespace
|
970
|
+
text = text.gsub(/\s+/, ' ')
|
971
|
+
|
972
|
+
# Remove special characters
|
973
|
+
text = text.gsub(/[^\w\s\.\,\!\?\-]/, '')
|
974
|
+
|
975
|
+
# Normalize line endings
|
976
|
+
text = text.gsub(/\r\n/, "\n")
|
977
|
+
|
978
|
+
text.strip
|
979
|
+
end
|
980
|
+
end
|
981
|
+
```
|
982
|
+
|
983
|
+
## Testing Custom Tools
|
984
|
+
|
985
|
+
### RSpec Tests for Tools
|
986
|
+
|
987
|
+
```ruby
|
988
|
+
require 'rspec'
|
989
|
+
|
990
|
+
RSpec.describe CalculatorTool do
|
991
|
+
let(:tool) { CalculatorTool.new(precision: 2) }
|
992
|
+
|
993
|
+
describe '#execute' do
|
994
|
+
context 'with addition' do
|
995
|
+
it 'adds numbers correctly' do
|
996
|
+
result = tool.execute(
|
997
|
+
operation: 'add',
|
998
|
+
operands: [5, 3, 2]
|
999
|
+
)
|
1000
|
+
expect(result).to eq('Result: 10')
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
context 'with division' do
|
1005
|
+
it 'divides numbers correctly' do
|
1006
|
+
result = tool.execute(
|
1007
|
+
operation: 'divide',
|
1008
|
+
operands: [10, 2]
|
1009
|
+
)
|
1010
|
+
expect(result).to eq('Result: 5')
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
it 'raises error for division by zero' do
|
1014
|
+
expect {
|
1015
|
+
tool.execute(
|
1016
|
+
operation: 'divide',
|
1017
|
+
operands: [10, 0]
|
1018
|
+
)
|
1019
|
+
}.to raise_error(RCrewAI::Tools::ToolError, /Division by zero/)
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
context 'with invalid parameters' do
|
1024
|
+
it 'raises error for missing required parameters' do
|
1025
|
+
expect {
|
1026
|
+
tool.execute(operation: 'add')
|
1027
|
+
}.to raise_error(RCrewAI::Tools::ToolError, /Missing required parameter/)
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
it 'raises error for invalid operands' do
|
1031
|
+
expect {
|
1032
|
+
tool.execute(
|
1033
|
+
operation: 'add',
|
1034
|
+
operands: 'not an array'
|
1035
|
+
)
|
1036
|
+
}.to raise_error(RCrewAI::Tools::ToolError, /must be an array/)
|
1037
|
+
end
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
```
|
1042
|
+
|
1043
|
+
### Integration Testing
|
1044
|
+
|
1045
|
+
```ruby
|
1046
|
+
RSpec.describe 'Tool Integration' do
|
1047
|
+
let(:agent) do
|
1048
|
+
RCrewAI::Agent.new(
|
1049
|
+
name: 'test_agent',
|
1050
|
+
role: 'Tool Tester',
|
1051
|
+
goal: 'Test tool integration',
|
1052
|
+
tools: [
|
1053
|
+
CalculatorTool.new,
|
1054
|
+
WeatherTool.new(api_key: 'test_key'),
|
1055
|
+
SessionMemoryTool.new
|
1056
|
+
]
|
1057
|
+
)
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
it 'agent can use multiple tools in sequence' do
|
1061
|
+
task = RCrewAI::Task.new(
|
1062
|
+
name: 'complex_calculation',
|
1063
|
+
description: 'Calculate 5 * 3, store result, then add 10',
|
1064
|
+
agent: agent
|
1065
|
+
)
|
1066
|
+
|
1067
|
+
# Mock the reasoning loop to use tools
|
1068
|
+
allow(agent).to receive(:reasoning_loop) do |task, context|
|
1069
|
+
# Step 1: Multiply
|
1070
|
+
result1 = agent.use_tool('calculator',
|
1071
|
+
operation: 'multiply',
|
1072
|
+
operands: [5, 3]
|
1073
|
+
)
|
1074
|
+
|
1075
|
+
# Step 2: Store result
|
1076
|
+
agent.use_tool('session_memory',
|
1077
|
+
action: 'store',
|
1078
|
+
key: 'multiply_result',
|
1079
|
+
value: 15
|
1080
|
+
)
|
1081
|
+
|
1082
|
+
# Step 3: Add
|
1083
|
+
result2 = agent.use_tool('calculator',
|
1084
|
+
operation: 'add',
|
1085
|
+
operands: [15, 10]
|
1086
|
+
)
|
1087
|
+
|
1088
|
+
"Final result: 25"
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
result = task.execute
|
1092
|
+
expect(result).to include('25')
|
1093
|
+
end
|
1094
|
+
end
|
1095
|
+
```
|
1096
|
+
|
1097
|
+
## Tool Security and Validation
|
1098
|
+
|
1099
|
+
### Secure Tool Base Class
|
1100
|
+
|
1101
|
+
```ruby
|
1102
|
+
class SecureTool < RCrewAI::Tools::Base
|
1103
|
+
def execute(**params)
|
1104
|
+
# Input sanitization
|
1105
|
+
sanitized_params = sanitize_inputs(params)
|
1106
|
+
|
1107
|
+
# Rate limiting
|
1108
|
+
check_rate_limit
|
1109
|
+
|
1110
|
+
# Execute with timeout
|
1111
|
+
Timeout::timeout(execution_timeout) do
|
1112
|
+
# Audit logging
|
1113
|
+
log_execution(sanitized_params)
|
1114
|
+
|
1115
|
+
# Execute tool logic
|
1116
|
+
result = perform_execution(sanitized_params)
|
1117
|
+
|
1118
|
+
# Output validation
|
1119
|
+
validate_output(result)
|
1120
|
+
|
1121
|
+
result
|
1122
|
+
end
|
1123
|
+
rescue Timeout::Error
|
1124
|
+
handle_timeout
|
1125
|
+
rescue => e
|
1126
|
+
handle_error(e)
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
private
|
1130
|
+
|
1131
|
+
def sanitize_inputs(params)
|
1132
|
+
params.transform_values do |value|
|
1133
|
+
case value
|
1134
|
+
when String
|
1135
|
+
# Remove potentially dangerous characters
|
1136
|
+
value.gsub(/[<>'\"&]/, '')
|
1137
|
+
when Array
|
1138
|
+
value.map { |v| sanitize_inputs(v) if v.is_a?(String) || v.is_a?(Hash) }
|
1139
|
+
when Hash
|
1140
|
+
sanitize_inputs(value)
|
1141
|
+
else
|
1142
|
+
value
|
1143
|
+
end
|
1144
|
+
end
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
def check_rate_limit
|
1148
|
+
@last_execution ||= {}
|
1149
|
+
key = "#{self.class.name}_#{Thread.current.object_id}"
|
1150
|
+
|
1151
|
+
if @last_execution[key] && Time.now - @last_execution[key] < 1
|
1152
|
+
raise ToolError, "Rate limit exceeded - please wait"
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
@last_execution[key] = Time.now
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
def validate_output(result)
|
1159
|
+
# Check output size
|
1160
|
+
if result.to_s.length > 1_000_000
|
1161
|
+
raise ToolError, "Output too large"
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
# Check for sensitive data
|
1165
|
+
if contains_sensitive_data?(result)
|
1166
|
+
raise ToolError, "Output contains sensitive information"
|
1167
|
+
end
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
def contains_sensitive_data?(text)
|
1171
|
+
patterns = [
|
1172
|
+
/\b\d{3}-\d{2}-\d{4}\b/, # SSN
|
1173
|
+
/\b\d{16}\b/, # Credit card
|
1174
|
+
/api[_-]?key/i, # API keys
|
1175
|
+
/password/i, # Passwords
|
1176
|
+
/secret/i # Secrets
|
1177
|
+
]
|
1178
|
+
|
1179
|
+
text_str = text.to_s
|
1180
|
+
patterns.any? { |pattern| text_str.match?(pattern) }
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def log_execution(params)
|
1184
|
+
@logger.info "Tool execution: #{@name}"
|
1185
|
+
@logger.debug "Parameters: #{params.inspect}"
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
def execution_timeout
|
1189
|
+
30 # Default 30 seconds
|
1190
|
+
end
|
1191
|
+
end
|
1192
|
+
```
|
1193
|
+
|
1194
|
+
## Best Practices
|
1195
|
+
|
1196
|
+
### 1. **Tool Design Principles**
|
1197
|
+
- Single responsibility - each tool does one thing well
|
1198
|
+
- Clear, descriptive names and descriptions
|
1199
|
+
- Comprehensive parameter validation
|
1200
|
+
- Meaningful error messages
|
1201
|
+
- Consistent output formatting
|
1202
|
+
|
1203
|
+
### 2. **Security Considerations**
|
1204
|
+
- Always sanitize inputs
|
1205
|
+
- Implement rate limiting
|
1206
|
+
- Use timeouts to prevent hanging
|
1207
|
+
- Validate output size and content
|
1208
|
+
- Audit log all executions
|
1209
|
+
- Never expose sensitive data
|
1210
|
+
|
1211
|
+
### 3. **Performance Optimization**
|
1212
|
+
- Cache expensive operations
|
1213
|
+
- Use connection pooling for databases/APIs
|
1214
|
+
- Implement retry logic with backoff
|
1215
|
+
- Stream large files instead of loading entirely
|
1216
|
+
- Clean up resources after use
|
1217
|
+
|
1218
|
+
### 4. **Error Handling**
|
1219
|
+
- Provide clear error messages
|
1220
|
+
- Distinguish between recoverable and fatal errors
|
1221
|
+
- Log errors with context
|
1222
|
+
- Implement graceful degradation
|
1223
|
+
- Return partial results when possible
|
1224
|
+
|
1225
|
+
### 5. **Testing Strategy**
|
1226
|
+
- Unit test all tool methods
|
1227
|
+
- Test parameter validation thoroughly
|
1228
|
+
- Mock external dependencies
|
1229
|
+
- Test error conditions
|
1230
|
+
- Integration test with agents
|
1231
|
+
- Performance test with large inputs
|
1232
|
+
|
1233
|
+
## Next Steps
|
1234
|
+
|
1235
|
+
Now that you can build custom tools:
|
1236
|
+
|
1237
|
+
1. Learn about [Working with Multiple Crews]({{ site.baseurl }}/tutorials/multiple-crews)
|
1238
|
+
2. Explore [Production Deployment]({{ site.baseurl }}/tutorials/deployment) strategies
|
1239
|
+
3. Review the [Tools API Documentation]({{ site.baseurl }}/api/tools)
|
1240
|
+
4. Check out [Example Custom Tools]({{ site.baseurl }}/examples/) in production
|
1241
|
+
|
1242
|
+
Custom tools are essential for extending RCrewAI to handle specialized tasks and integrate with your existing systems.
|