rcrewai 0.2.0 → 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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +24 -0
  5. data/README.md +33 -1
  6. data/Rakefile +53 -53
  7. data/bin/rcrewai +3 -3
  8. data/docs/mcp.md +109 -0
  9. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  10. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  11. data/docs/upgrading-to-0.3.md +163 -0
  12. data/examples/async_execution_example.rb +82 -81
  13. data/examples/hierarchical_crew_example.rb +68 -72
  14. data/examples/human_in_the_loop_example.rb +73 -74
  15. data/examples/mcp_example.rb +48 -0
  16. data/examples/native_tools_example.rb +64 -0
  17. data/examples/streaming_example.rb +56 -0
  18. data/lib/rcrewai/agent.rb +148 -287
  19. data/lib/rcrewai/async_executor.rb +43 -43
  20. data/lib/rcrewai/cli.rb +11 -11
  21. data/lib/rcrewai/configuration.rb +14 -9
  22. data/lib/rcrewai/crew.rb +56 -39
  23. data/lib/rcrewai/events.rb +30 -0
  24. data/lib/rcrewai/human_input.rb +104 -114
  25. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  26. data/lib/rcrewai/llm_client.rb +1 -1
  27. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  28. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  29. data/lib/rcrewai/llm_clients/base.rb +11 -7
  30. data/lib/rcrewai/llm_clients/google.rb +159 -95
  31. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  32. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  33. data/lib/rcrewai/mcp/client.rb +101 -0
  34. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  35. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  36. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  37. data/lib/rcrewai/mcp.rb +8 -0
  38. data/lib/rcrewai/memory.rb +45 -37
  39. data/lib/rcrewai/pricing.rb +34 -0
  40. data/lib/rcrewai/process.rb +86 -95
  41. data/lib/rcrewai/provider_schema.rb +38 -0
  42. data/lib/rcrewai/sse_parser.rb +55 -0
  43. data/lib/rcrewai/task.rb +56 -64
  44. data/lib/rcrewai/tool_runner.rb +132 -0
  45. data/lib/rcrewai/tool_schema.rb +97 -0
  46. data/lib/rcrewai/tools/base.rb +98 -37
  47. data/lib/rcrewai/tools/code_executor.rb +71 -74
  48. data/lib/rcrewai/tools/email_sender.rb +70 -78
  49. data/lib/rcrewai/tools/file_reader.rb +38 -30
  50. data/lib/rcrewai/tools/file_writer.rb +40 -38
  51. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  52. data/lib/rcrewai/tools/sql_database.rb +58 -55
  53. data/lib/rcrewai/tools/web_search.rb +26 -25
  54. data/lib/rcrewai/version.rb +2 -2
  55. data/lib/rcrewai.rb +18 -10
  56. data/rcrewai.gemspec +55 -36
  57. metadata +86 -50
@@ -1,32 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../tool_schema'
4
+
3
5
  module RCrewAI
4
6
  module Tools
5
7
  class Base
6
- attr_reader :name, :description
8
+ extend RCrewAI::ToolSchema
7
9
 
8
10
  def initialize
9
- @name = self.class.name.split('::').last.downcase
10
- @description = "Base tool class"
11
+ # @name and @description are no longer set here.
12
+ # Instance #name and #description delegate to the class-level DSL
13
+ # (tool_name / description) via the fallback in the reader methods below.
14
+ end
15
+
16
+ def name
17
+ @name || self.class.tool_name
18
+ end
19
+
20
+ def description
21
+ @description || self.class.description
22
+ end
23
+
24
+ def json_schema
25
+ self.class.json_schema
26
+ end
27
+
28
+ def execute_with_validation(args_hash)
29
+ coerced = {}
30
+ schema_params = self.class.params
31
+
32
+ if schema_params.empty?
33
+ coerced = args_hash.transform_keys(&:to_sym)
34
+ return execute(**coerced)
35
+ end
36
+
37
+ schema_params.each do |p|
38
+ key_str = p[:name].to_s
39
+ key_sym = p[:name].to_sym
40
+ if args_hash.key?(key_str)
41
+ raw = args_hash[key_str]
42
+ elsif args_hash.key?(key_sym)
43
+ raw = args_hash[key_sym]
44
+ else
45
+ raise ToolError, "missing required param: #{p[:name]}" if p[:required]
46
+
47
+ next
48
+ end
49
+ coerced[key_sym] = coerce(raw, p[:type], p[:name])
50
+ end
51
+
52
+ execute(**coerced)
11
53
  end
12
54
 
13
55
  def execute(**params)
14
- raise NotImplementedError, "Subclasses must implement #execute method"
56
+ raise NotImplementedError, 'Subclasses must implement #execute method'
15
57
  end
16
58
 
17
59
  def validate_params!(params, required: [], optional: [])
18
60
  # Check required parameters
19
61
  missing = required - params.keys
20
- unless missing.empty?
21
- raise ToolError, "Missing required parameters: #{missing.join(', ')}"
22
- end
62
+ raise ToolError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
23
63
 
24
64
  # Check for unexpected parameters
25
65
  allowed = required + optional
26
66
  unexpected = params.keys - allowed
27
- unless unexpected.empty?
28
- raise ToolError, "Unexpected parameters: #{unexpected.join(', ')}"
29
- end
67
+ return if unexpected.empty?
68
+
69
+ raise ToolError, "Unexpected parameters: #{unexpected.join(', ')}"
30
70
  end
31
71
 
32
72
  def self.available_tools
@@ -43,40 +83,61 @@ module RCrewAI
43
83
 
44
84
  def self.create_tool(tool_name, **options)
45
85
  tool_class = case tool_name.to_s.downcase
46
- when 'websearch', 'web_search'
47
- WebSearch
48
- when 'filereader', 'file_reader'
49
- FileReader
50
- when 'filewriter', 'file_writer'
51
- FileWriter
52
- when 'sqldatabase', 'sql_database', 'database'
53
- SqlDatabase
54
- when 'emailsender', 'email_sender', 'email'
55
- EmailSender
56
- when 'codeexecutor', 'code_executor', 'code'
57
- CodeExecutor
58
- when 'pdfprocessor', 'pdf_processor', 'pdf'
59
- PdfProcessor
60
- else
61
- raise ToolError, "Unknown tool: #{tool_name}"
62
- end
86
+ when 'websearch', 'web_search'
87
+ WebSearch
88
+ when 'filereader', 'file_reader'
89
+ FileReader
90
+ when 'filewriter', 'file_writer'
91
+ FileWriter
92
+ when 'sqldatabase', 'sql_database', 'database'
93
+ SqlDatabase
94
+ when 'emailsender', 'email_sender', 'email'
95
+ EmailSender
96
+ when 'codeexecutor', 'code_executor', 'code'
97
+ CodeExecutor
98
+ when 'pdfprocessor', 'pdf_processor', 'pdf'
99
+ PdfProcessor
100
+ else
101
+ raise ToolError, "Unknown tool: #{tool_name}"
102
+ end
63
103
 
64
104
  tool_class.new(**options)
65
105
  end
66
106
 
67
107
  def self.list_available_tools
68
- {
69
- 'websearch' => 'Search the web using DuckDuckGo',
70
- 'filereader' => 'Read contents from text files',
71
- 'filewriter' => 'Write content to text files',
72
- 'sqldatabase' => 'Execute SQL queries against databases',
73
- 'emailsender' => 'Send emails via SMTP',
74
- 'codeexecutor' => 'Execute code in various programming languages',
75
- 'pdfprocessor' => 'Read and extract text from PDF files'
76
- }
108
+ available_tools.each_with_object({}) do |klass, h|
109
+ h[klass.tool_name] = klass.description
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def coerce(value, type, name)
116
+ case type
117
+ when :integer
118
+ return value if value.is_a?(Integer)
119
+
120
+ Integer(value.to_s)
121
+ when :number
122
+ return value if value.is_a?(Numeric)
123
+
124
+ Float(value.to_s)
125
+ when :boolean
126
+ return value if [true, false].include?(value)
127
+
128
+ %w[true 1 yes].include?(value.to_s.downcase)
129
+ when :string, :enum
130
+ value.to_s
131
+ when :array, :object
132
+ value
133
+ else
134
+ value
135
+ end
136
+ rescue ArgumentError, TypeError
137
+ raise ToolError, "#{name} must be #{type}, got #{value.inspect}"
77
138
  end
78
139
  end
79
140
 
80
141
  class ToolError < RCrewAI::Error; end
81
142
  end
82
- end
143
+ end
@@ -8,12 +8,21 @@ require 'fileutils'
8
8
  module RCrewAI
9
9
  module Tools
10
10
  class CodeExecutor < Base
11
+ tool_name 'code_executor'
12
+ description 'Execute code in a sandboxed subprocess'
13
+ param :code, type: :string, required: true, description: 'Source code to run'
14
+ param :language, type: :enum, required: true,
15
+ values: %w[ruby python javascript bash],
16
+ description: 'Language: ruby, python, javascript, or bash'
17
+ param :args, type: :array, required: false, items: { type: :string },
18
+ description: 'Optional command-line arguments to pass to the interpreter'
19
+ param :stdin, type: :string, required: false,
20
+ description: 'Optional stdin payload'
21
+
11
22
  def initialize(**options)
12
23
  super()
13
- @name = 'codeexecutor'
14
- @description = 'Execute code in various programming languages (Python, Ruby, JavaScript, etc.)'
15
24
  @timeout = options.fetch(:timeout, 30)
16
- @max_output_size = options.fetch(:max_output_size, 100_000) # 100KB
25
+ @max_output_size = options.fetch(:max_output_size, 100_000) # 100KB
17
26
  @allowed_languages = options.fetch(:allowed_languages, %w[python ruby javascript bash])
18
27
  @working_directory = options[:working_directory] || Dir.mktmpdir('rcrewai_code_')
19
28
  @enable_file_operations = options.fetch(:enable_file_operations, false)
@@ -21,18 +30,18 @@ module RCrewAI
21
30
  end
22
31
 
23
32
  def execute(**params)
24
- validate_params!(params, required: [:code, :language], optional: [:args, :stdin])
25
-
33
+ validate_params!(params, required: %i[code language], optional: %i[args stdin])
34
+
26
35
  language = params[:language].to_s.downcase
27
36
  code = params[:code]
28
37
  args = params[:args] || []
29
38
  stdin_input = params[:stdin]
30
-
39
+
31
40
  begin
32
41
  validate_execution_params!(language, code)
33
42
  result = execute_code(language, code, args, stdin_input)
34
43
  format_execution_result(language, code, result)
35
- rescue => e
44
+ rescue StandardError => e
36
45
  "Code execution failed: #{e.message}"
37
46
  end
38
47
  end
@@ -43,7 +52,7 @@ module RCrewAI
43
52
  # Create secure working directory
44
53
  FileUtils.mkdir_p(@working_directory)
45
54
  File.chmod(0o755, @working_directory)
46
-
55
+
47
56
  # Set environment variables for security
48
57
  @secure_env = {
49
58
  'PATH' => '/usr/local/bin:/usr/bin:/bin',
@@ -58,63 +67,57 @@ module RCrewAI
58
67
  unless @allowed_languages.include?(language)
59
68
  raise ToolError, "Language not allowed: #{language}. Allowed: #{@allowed_languages.join(', ')}"
60
69
  end
61
-
62
- if code.to_s.strip.empty?
63
- raise ToolError, "Code cannot be empty"
64
- end
65
-
66
- if code.length > 50_000 # 50KB max
67
- raise ToolError, "Code too long: #{code.length} characters (max: 50,000)"
68
- end
69
-
70
+
71
+ raise ToolError, 'Code cannot be empty' if code.to_s.strip.empty?
72
+
73
+ raise ToolError, "Code too long: #{code.length} characters (max: 50,000)" if code.length > 50_000 # 50KB max
74
+
70
75
  validate_code_safety!(language, code)
71
76
  end
72
77
 
73
78
  def validate_code_safety!(language, code)
74
79
  code_lower = code.downcase
75
-
80
+
76
81
  # Common dangerous patterns across languages
77
82
  dangerous_patterns = [
78
83
  # System operations
79
84
  'system(', 'exec(', 'eval(', 'subprocess', 'os.system',
80
85
  'shell_exec', 'passthru', '`', 'require "open3"',
81
-
86
+
82
87
  # File operations (if not explicitly enabled)
83
88
  'file.open', 'open(', 'with open', 'fopen', 'file_get_contents',
84
89
  'file_put_contents', 'unlink', 'delete', 'remove',
85
-
90
+
86
91
  # Network operations
87
92
  'socket', 'http', 'urllib', 'requests', 'net::',
88
93
  'tcpsocket', 'udpsocket', 'curl', 'wget',
89
-
94
+
90
95
  # Process operations
91
96
  'fork', 'spawn', 'thread', 'process',
92
-
97
+
93
98
  # Dangerous Python imports
94
99
  'import os', 'import sys', 'import subprocess', 'import socket',
95
100
  'from os', 'from sys', 'from subprocess',
96
-
101
+
97
102
  # Dangerous Ruby requires
98
103
  'require "fileutils"', 'require "open3"', 'require "net/',
99
-
104
+
100
105
  # Shell commands
101
106
  'rm -', 'sudo', 'chmod', 'chown', 'dd if=', 'mkfs',
102
107
  'iptables', 'systemctl', 'service '
103
108
  ]
104
-
109
+
105
110
  unless @enable_file_operations
106
111
  dangerous_patterns += [
107
112
  'file.', 'open(', 'with open', 'fopen', 'file_get_contents',
108
113
  'file_put_contents', 'fileutils', 'fs.', 'path.', 'dir.'
109
114
  ]
110
115
  end
111
-
116
+
112
117
  dangerous_patterns.each do |pattern|
113
- if code_lower.include?(pattern)
114
- raise ToolError, "Potentially dangerous code detected: #{pattern}"
115
- end
118
+ raise ToolError, "Potentially dangerous code detected: #{pattern}" if code_lower.include?(pattern)
116
119
  end
117
-
120
+
118
121
  # Language-specific checks
119
122
  case language
120
123
  when 'python'
@@ -135,11 +138,9 @@ module RCrewAI
135
138
  'compile(', 'exec(', 'eval(',
136
139
  'input(', 'raw_input('
137
140
  ]
138
-
141
+
139
142
  python_dangerous.each do |pattern|
140
- if code.include?(pattern)
141
- raise ToolError, "Dangerous Python construct: #{pattern}"
142
- end
143
+ raise ToolError, "Dangerous Python construct: #{pattern}" if code.include?(pattern)
143
144
  end
144
145
  end
145
146
 
@@ -150,11 +151,9 @@ module RCrewAI
150
151
  'const_get', 'const_set', 'remove_const',
151
152
  'gets', 'readline', 'readlines'
152
153
  ]
153
-
154
+
154
155
  ruby_dangerous.each do |pattern|
155
- if code.include?(pattern)
156
- raise ToolError, "Dangerous Ruby construct: #{pattern}"
157
- end
156
+ raise ToolError, "Dangerous Ruby construct: #{pattern}" if code.include?(pattern)
158
157
  end
159
158
  end
160
159
 
@@ -165,11 +164,9 @@ module RCrewAI
165
164
  'document.', 'window.', 'location.',
166
165
  'settimeout', 'setinterval'
167
166
  ]
168
-
167
+
169
168
  js_dangerous.each do |pattern|
170
- if code.include?(pattern)
171
- raise ToolError, "Dangerous JavaScript construct: #{pattern}"
172
- end
169
+ raise ToolError, "Dangerous JavaScript construct: #{pattern}" if code.include?(pattern)
173
170
  end
174
171
  end
175
172
 
@@ -180,11 +177,9 @@ module RCrewAI
180
177
  '$(', '`', 'eval ', 'exec ',
181
178
  '/etc/', '/proc/', '/sys/', '/dev/'
182
179
  ]
183
-
180
+
184
181
  bash_dangerous.each do |pattern|
185
- if code.include?(pattern)
186
- raise ToolError, "Dangerous bash construct: #{pattern}"
187
- end
182
+ raise ToolError, "Dangerous bash construct: #{pattern}" if code.include?(pattern)
188
183
  end
189
184
  end
190
185
 
@@ -240,15 +235,15 @@ module RCrewAI
240
235
  temp_file.close
241
236
  yield temp_file.path
242
237
  ensure
243
- temp_file.unlink if temp_file
238
+ temp_file&.unlink
244
239
  end
245
240
  end
246
241
 
247
242
  def execute_command(command, stdin_input)
248
- stdout_str = ""
249
- stderr_str = ""
243
+ stdout_str = ''
244
+ stderr_str = ''
250
245
  exit_status = nil
251
-
246
+
252
247
  # Execute with timeout and security restrictions
253
248
  Open3.popen3(@secure_env, *command, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
254
249
  # Write stdin if provided
@@ -256,41 +251,45 @@ module RCrewAI
256
251
  stdin.write(stdin_input)
257
252
  stdin.close
258
253
  end
259
-
254
+
260
255
  # Set up timeout
261
256
  timeout_thread = Thread.new do
262
257
  sleep @timeout
263
- Process.kill('KILL', wait_thread.pid) rescue nil
258
+ begin
259
+ Process.kill('KILL', wait_thread.pid)
260
+ rescue StandardError
261
+ nil
262
+ end
264
263
  end
265
-
264
+
266
265
  begin
267
266
  # Read output
268
267
  stdout_str = read_with_limit(stdout, @max_output_size)
269
268
  stderr_str = read_with_limit(stderr, @max_output_size)
270
-
269
+
271
270
  exit_status = wait_thread.value.exitstatus
272
- rescue => e
271
+ rescue StandardError => e
273
272
  exit_status = -1
274
273
  stderr_str += "\nExecution error: #{e.message}"
275
274
  ensure
276
275
  timeout_thread.kill
277
276
  end
278
277
  end
279
-
278
+
280
279
  {
281
280
  stdout: stdout_str,
282
281
  stderr: stderr_str,
283
282
  exit_status: exit_status,
284
- success: exit_status == 0
283
+ success: exit_status.zero?
285
284
  }
286
285
  end
287
286
 
288
287
  def read_with_limit(io, limit)
289
- output = ""
288
+ output = ''
290
289
  while output.bytesize < limit && !io.eof?
291
290
  chunk = io.read(1024)
292
291
  break unless chunk
293
-
292
+
294
293
  if output.bytesize + chunk.bytesize > limit
295
294
  remaining = limit - output.bytesize
296
295
  output += chunk[0, remaining]
@@ -303,31 +302,29 @@ module RCrewAI
303
302
  output
304
303
  end
305
304
 
306
- def format_execution_result(language, code, result)
305
+ def format_execution_result(language, _code, result)
307
306
  output = []
308
307
  output << "Code Execution Result (#{language.upcase})"
309
- output << "=" * 40
308
+ output << '=' * 40
310
309
  output << "Exit Status: #{result[:exit_status]} (#{result[:success] ? 'SUCCESS' : 'FAILURE'})"
311
- output << ""
312
-
310
+ output << ''
311
+
313
312
  if result[:stdout] && !result[:stdout].empty?
314
- output << "STDOUT:"
313
+ output << 'STDOUT:'
315
314
  output << result[:stdout]
316
- output << ""
315
+ output << ''
317
316
  end
318
-
317
+
319
318
  if result[:stderr] && !result[:stderr].empty?
320
- output << "STDERR:"
319
+ output << 'STDERR:'
321
320
  output << result[:stderr]
322
- output << ""
323
- end
324
-
325
- if result[:stdout].empty? && result[:stderr].empty?
326
- output << "No output produced."
321
+ output << ''
327
322
  end
328
-
323
+
324
+ output << 'No output produced.' if result[:stdout].empty? && result[:stderr].empty?
325
+
329
326
  output.join("\n")
330
327
  end
331
328
  end
332
329
  end
333
- end
330
+ end