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
data/lib/rcrewai/task.rb CHANGED
@@ -19,13 +19,13 @@ module RCrewAI
19
19
  @tools = options[:tools] || [] # Additional tools for this specific task
20
20
  @async = options[:async] || false # Whether task can run asynchronously
21
21
  @callback = options[:callback] # Callback function after completion
22
-
22
+
23
23
  # Human interaction options
24
24
  @human_input_enabled = options[:human_input] || false
25
25
  @require_human_confirmation = options[:require_confirmation] || false
26
26
  @allow_human_guidance = options[:allow_guidance] || false
27
27
  @human_review_points = options[:human_review_points] || []
28
-
28
+
29
29
  @result = nil
30
30
  @status = :pending
31
31
  @start_time = nil
@@ -53,7 +53,7 @@ module RCrewAI
53
53
  if agent
54
54
  validate_dependencies!
55
55
  provide_context_to_agent
56
-
56
+
57
57
  # Enable human input for agent if task allows it
58
58
  if @human_input_enabled && agent.respond_to?(:enable_human_input)
59
59
  agent.enable_human_input(
@@ -61,31 +61,28 @@ module RCrewAI
61
61
  require_approval_for_final_answer: @allow_human_guidance
62
62
  )
63
63
  end
64
-
64
+
65
65
  @result = agent.execute_task(self)
66
-
66
+
67
67
  # Post-execution human review if configured
68
68
  if @human_input_enabled && @human_review_points.include?(:completion)
69
69
  review_result = request_task_completion_review
70
- if review_result && !review_result[:approved]
71
- @result = handle_completion_review_feedback(review_result)
72
- end
70
+ @result = handle_completion_review_feedback(review_result) if review_result && !review_result[:approved]
73
71
  end
74
-
72
+
75
73
  @status = :completed
76
74
  else
77
- @result = "Task requires an agent"
75
+ @result = 'Task requires an agent'
78
76
  @status = :failed
79
77
  end
80
78
 
81
79
  @end_time = Time.now
82
80
  @execution_time = @end_time - @start_time
83
-
81
+
84
82
  # Execute callback if provided
85
83
  @callback&.call(self, @result)
86
-
87
- @result
88
84
 
85
+ @result
89
86
  rescue TaskDependencyError => e
90
87
  # Don't retry dependency errors - they need dependencies to be completed first
91
88
  @status = :failed
@@ -93,16 +90,16 @@ module RCrewAI
93
90
  @execution_time = @end_time - @start_time if @start_time
94
91
  @result = "Task dependencies not met: #{e.message}"
95
92
  raise e
96
- rescue => e
93
+ rescue StandardError => e
97
94
  @status = :failed
98
95
  @end_time = Time.now
99
96
  @execution_time = @end_time - @start_time if @start_time
100
-
97
+
101
98
  # Retry logic with human intervention
102
99
  if @retry_count < @max_retries
103
100
  @retry_count += 1
104
101
  puts "Task #{name} failed, retrying (#{@retry_count}/#{@max_retries})"
105
-
102
+
106
103
  # Ask human for retry guidance if enabled
107
104
  if @human_input_enabled
108
105
  retry_decision = request_retry_guidance(e)
@@ -113,15 +110,13 @@ module RCrewAI
113
110
  when 'modify'
114
111
  # Allow human to modify task parameters
115
112
  modification_result = request_task_modification
116
- if modification_result[:modified]
117
- apply_task_modifications(modification_result[:changes])
118
- end
119
- # 'retry' is the default - continue with retry logic
113
+ apply_task_modifications(modification_result[:changes]) if modification_result[:modified]
114
+ # 'retry' is the default - continue with retry logic
120
115
  end
121
116
  end
122
-
117
+
123
118
  @status = :pending
124
- sleep(2 ** @retry_count) # Exponential backoff
119
+ sleep(2**@retry_count) # Exponential backoff
125
120
  return execute
126
121
  end
127
122
 
@@ -129,14 +124,12 @@ module RCrewAI
129
124
  raise TaskExecutionError, "Task '#{name}' failed: #{e.message}"
130
125
  ensure
131
126
  # Disable human input for agent after task completion
132
- if @human_input_enabled && agent.respond_to?(:disable_human_input)
133
- agent.disable_human_input
134
- end
127
+ agent.disable_human_input if @human_input_enabled && agent.respond_to?(:disable_human_input)
135
128
  end
136
129
  end
137
130
 
138
131
  def context_data
139
- return "" if context.empty?
132
+ return '' if context.empty?
140
133
 
141
134
  context_results = context.map do |task|
142
135
  if task.completed?
@@ -200,13 +193,12 @@ module RCrewAI
200
193
  def confirm_task_execution
201
194
  message = "Confirm execution of task: #{name}"
202
195
  context = "Description: #{description}\nExpected Output: #{expected_output || 'Not specified'}\nAssigned Agent: #{agent&.name || 'No agent'}"
203
- consequences = "The task will be executed with the specified agent and may use external tools."
204
-
196
+ consequences = 'The task will be executed with the specified agent and may use external tools.'
197
+
205
198
  request_human_approval(message,
206
- context: context,
207
- consequences: consequences,
208
- timeout: 60
209
- )
199
+ context: context,
200
+ consequences: consequences,
201
+ timeout: 60)
210
202
  end
211
203
 
212
204
  def request_task_completion_review
@@ -214,30 +206,30 @@ module RCrewAI
214
206
  Task: #{name}
215
207
  Description: #{description}
216
208
  Expected Output: #{expected_output || 'Not specified'}
217
-
209
+
218
210
  Actual Result:
219
211
  #{result}
220
-
212
+
221
213
  Execution Time: #{execution_time&.round(2)} seconds
222
214
  Agent: #{agent&.name || 'Unknown'}
223
215
  CONTENT
224
216
 
225
217
  request_human_review(
226
218
  review_content,
227
- review_criteria: ["Accuracy", "Completeness", "Meets expectations"],
219
+ review_criteria: ['Accuracy', 'Completeness', 'Meets expectations'],
228
220
  timeout: 120
229
221
  )
230
222
  end
231
223
 
232
224
  def handle_completion_review_feedback(review_result)
233
- if review_result[:suggested_changes] && review_result[:suggested_changes].any?
225
+ if review_result[:suggested_changes]&.any?
234
226
  # Ask human how to handle the feedback
235
227
  choice_result = request_human_choice(
236
- "Task completed but received feedback. How should I proceed?",
228
+ 'Task completed but received feedback. How should I proceed?',
237
229
  [
238
- "Accept result as-is despite feedback",
239
- "Request agent revision based on feedback",
240
- "Let me provide the correct result"
230
+ 'Accept result as-is despite feedback',
231
+ 'Request agent revision based on feedback',
232
+ 'Let me provide the correct result'
241
233
  ],
242
234
  timeout: 60
243
235
  )
@@ -247,13 +239,13 @@ module RCrewAI
247
239
  result # Return original result
248
240
  when 1
249
241
  # Request agent to revise (simplified)
250
- revised_result = "#{result}\n\nNote: Human feedback received: #{review_result[:feedback]}"
251
- revised_result
242
+ "#{result}\n\nNote: Human feedback received: #{review_result[:feedback]}"
243
+
252
244
  when 2
253
245
  # Get corrected result from human
254
246
  correction_result = request_human_input(
255
- "Please provide the corrected task result:",
256
- help_text: "Enter the complete, corrected result for this task"
247
+ 'Please provide the corrected task result:',
248
+ help_text: 'Enter the complete, corrected result for this task'
257
249
  )
258
250
  correction_result[:input] || result
259
251
  else
@@ -266,9 +258,9 @@ module RCrewAI
266
258
 
267
259
  def request_retry_guidance(error)
268
260
  choices = [
269
- "Retry with current settings",
270
- "Modify task parameters and retry",
271
- "Abort task execution"
261
+ 'Retry with current settings',
262
+ 'Modify task parameters and retry',
263
+ 'Abort task execution'
272
264
  ]
273
265
 
274
266
  choice_result = request_human_choice(
@@ -290,9 +282,9 @@ module RCrewAI
290
282
 
291
283
  def request_task_modification
292
284
  modification_input = request_human_input(
293
- "Please specify task modifications (JSON format):",
285
+ 'Please specify task modifications (JSON format):',
294
286
  type: :json,
295
- help_text: "Provide modifications as JSON, e.g. {\"description\": \"new description\", \"expected_output\": \"new output\"}"
287
+ help_text: 'Provide modifications as JSON, e.g. {"description": "new description", "expected_output": "new output"}'
296
288
  )
297
289
 
298
290
  if modification_input[:valid]
@@ -316,38 +308,38 @@ module RCrewAI
316
308
  when 'expected_output'
317
309
  @expected_output = value
318
310
  when 'max_retries'
319
- @max_retries = value.to_i if value.to_i > 0
311
+ @max_retries = value.to_i if value.to_i.positive?
320
312
  end
321
313
  end
322
314
  end
323
315
 
324
316
  def validate_dependencies!
325
- unless dependencies_met?
326
- incomplete_deps = context.reject(&:completed?).map(&:name)
327
- raise TaskDependencyError, "Dependencies not met: #{incomplete_deps.join(', ')}"
328
- end
317
+ return if dependencies_met?
318
+
319
+ incomplete_deps = context.reject(&:completed?).map(&:name)
320
+ raise TaskDependencyError, "Dependencies not met: #{incomplete_deps.join(', ')}"
329
321
  end
330
322
 
331
323
  def provide_context_to_agent
332
324
  # Temporarily add task-specific tools to agent
333
325
  original_tools = agent.tools.dup
334
326
  combined_tools = (agent.tools + @tools).uniq
335
-
327
+
336
328
  # Use metaprogramming to temporarily extend agent's tools
337
329
  agent.instance_variable_set(:@tools, combined_tools)
338
-
330
+
339
331
  # Restore original tools after execution (handled by ensure in execute method)
340
332
  at_exit { agent.instance_variable_set(:@tools, original_tools) }
341
333
  end
342
334
 
343
335
  class CLI < Thor
344
- desc "new NAME", "Create a new task"
336
+ desc 'new NAME', 'Create a new task'
345
337
  option :description, type: :string, required: true
346
338
  option :agent, type: :string
347
339
  option :expected_output, type: :string
348
340
  option :async, type: :boolean, default: false
349
341
  def new(name)
350
- task = Task.new(
342
+ Task.new(
351
343
  name: name,
352
344
  description: options[:description],
353
345
  agent: options[:agent],
@@ -361,16 +353,16 @@ module RCrewAI
361
353
  puts "Async: #{options[:async]}"
362
354
  end
363
355
 
364
- desc "list", "List all tasks"
356
+ desc 'list', 'List all tasks'
365
357
  def list
366
- puts "Available tasks:"
367
- puts " - research_topic (Agent: researcher)"
368
- puts " - write_article (Agent: writer)"
369
- puts " - analyze_data (Agent: analyst)"
358
+ puts 'Available tasks:'
359
+ puts ' - research_topic (Agent: researcher)'
360
+ puts ' - write_article (Agent: writer)'
361
+ puts ' - analyze_data (Agent: analyst)'
370
362
  end
371
363
  end
372
364
  end
373
365
 
374
366
  class TaskExecutionError < Error; end
375
367
  class TaskDependencyError < TaskExecutionError; end
376
- end
368
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'events'
4
+ require_relative 'provider_schema'
5
+
6
+ module RCrewAI
7
+ class ToolRunner
8
+ DEFAULT_MAX_ITERATIONS = 10
9
+
10
+ def initialize(agent:, llm:, tools:, max_iterations: DEFAULT_MAX_ITERATIONS, event_sink: nil)
11
+ @agent = agent
12
+ @llm = llm
13
+ @tools = tools
14
+ @tools_by_name = tools.each_with_object({}) { |t, h| h[t.name] = t }
15
+ @max_iterations = max_iterations
16
+ @sink = event_sink || ->(_) {}
17
+ end
18
+
19
+ def run(messages:) # rubocop:disable Metrics/AbcSize
20
+ msgs = messages.dup
21
+ history = []
22
+ iter = 0
23
+ total_usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
24
+
25
+ while iter < @max_iterations
26
+ iter += 1
27
+ emit(Events::IterationStart, iteration: iter, iteration_index: iter)
28
+
29
+ response = @llm.chat(
30
+ messages: msgs,
31
+ tools: @tools.map(&:json_schema),
32
+ stream: ->(e) { @sink.call(retag(e, iter)) }
33
+ )
34
+ accumulate_usage(total_usage, response[:usage])
35
+
36
+ if response[:tool_calls].nil? || response[:tool_calls].empty?
37
+ emit(Events::IterationEnd, iteration: iter, finish_reason: response[:finish_reason])
38
+ return finalize(content: response[:content], history: history, iter: iter,
39
+ finish_reason: response[:finish_reason], usage: total_usage)
40
+ end
41
+
42
+ msgs << { role: 'assistant', content: response[:content], tool_calls: response[:tool_calls] }
43
+
44
+ response[:tool_calls].each do |tc|
45
+ tool = @tools_by_name[tc[:name]]
46
+ emit(Events::ToolCallStart, iteration: iter,
47
+ tool: tc[:name], args: tc[:arguments], call_id: tc[:id])
48
+
49
+ if tool.nil?
50
+ err = "tool not found: #{tc[:name]}"
51
+ emit(Events::ToolCallError, iteration: iter,
52
+ tool: tc[:name], call_id: tc[:id], error: err)
53
+ msgs << tool_result_message(tc[:id], "ERROR: #{err}")
54
+ next
55
+ end
56
+
57
+ started = monotonic_ms
58
+ begin
59
+ result = tool.execute_with_validation(tc[:arguments] || {})
60
+ duration = monotonic_ms - started
61
+ @agent.memory.add_tool_usage(tc[:name], tc[:arguments], result) if @agent.respond_to?(:memory) && @agent.memory
62
+ emit(Events::ToolCallResult, iteration: iter,
63
+ tool: tc[:name], call_id: tc[:id], result: result,
64
+ duration_ms: duration)
65
+ history << { tool: tc[:name], args: tc[:arguments], result: result, duration_ms: duration }
66
+ msgs << tool_result_message(tc[:id], result.to_s)
67
+ rescue StandardError => e
68
+ emit(Events::ToolCallError, iteration: iter,
69
+ tool: tc[:name], call_id: tc[:id], error: e.message)
70
+ msgs << tool_result_message(tc[:id], "ERROR: #{e.message}")
71
+ end
72
+ end
73
+
74
+ emit(Events::IterationEnd, iteration: iter, finish_reason: :tool_calls)
75
+ end
76
+
77
+ finalize(content: nil, history: history, iter: iter,
78
+ finish_reason: :max_iterations, usage: total_usage)
79
+ end
80
+
81
+ private
82
+
83
+ def tool_result_message(call_id, content)
84
+ { role: 'tool', tool_call_id: call_id, content: content }
85
+ end
86
+
87
+ def emit(klass, iteration:, **attrs)
88
+ type_sym = klass.name.split('::').last
89
+ .gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
90
+ .sub(/^_/, '').to_sym
91
+ @sink.call(klass.new(
92
+ type: type_sym,
93
+ timestamp: Time.now,
94
+ agent: agent_name,
95
+ iteration: iteration,
96
+ **attrs
97
+ ))
98
+ end
99
+
100
+ def agent_name
101
+ @agent.respond_to?(:name) ? @agent.name : nil
102
+ end
103
+
104
+ def retag(event, iter)
105
+ event.agent = agent_name if event.respond_to?(:agent=) && event.agent.nil?
106
+ event.iteration = iter if event.respond_to?(:iteration=) && event.iteration.nil?
107
+ event
108
+ end
109
+
110
+ def accumulate_usage(total, partial)
111
+ return unless partial.is_a?(Hash)
112
+
113
+ total[:prompt_tokens] += partial[:prompt_tokens] || 0
114
+ total[:completion_tokens] += partial[:completion_tokens] || 0
115
+ total[:total_tokens] += partial[:total_tokens] || 0
116
+ end
117
+
118
+ def finalize(content:, history:, iter:, finish_reason:, usage:)
119
+ {
120
+ content: content,
121
+ tool_calls_history: history,
122
+ usage: usage,
123
+ iterations: iter,
124
+ finish_reason: finish_reason
125
+ }
126
+ end
127
+
128
+ def monotonic_ms
129
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) * 1000).to_i
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module ToolSchema
5
+ TYPE_MAP = {
6
+ string: 'string', integer: 'integer', number: 'number',
7
+ boolean: 'boolean', array: 'array', object: 'object', enum: 'string'
8
+ }.freeze
9
+
10
+ def self.extended(base)
11
+ base.instance_variable_set(:@params, [])
12
+ base.instance_variable_set(:@tool_name, nil)
13
+ base.instance_variable_set(:@description, nil)
14
+ end
15
+
16
+ def tool_name(name = nil)
17
+ return @tool_name || name_default if name.nil?
18
+
19
+ @tool_name = name.to_s
20
+ end
21
+
22
+ def description(desc = nil)
23
+ return @description || '' if desc.nil?
24
+
25
+ @description = desc.to_s
26
+ end
27
+
28
+ def param(name, type:, required: false, default: nil, description: nil, # rubocop:disable Metrics/ParameterLists
29
+ items: nil, values: nil, properties: nil)
30
+ @params ||= []
31
+ @params << {
32
+ name: name, type: type, required: required, default: default,
33
+ description: description, items: items, values: values, properties: properties
34
+ }
35
+ end
36
+
37
+ def params
38
+ @params || []
39
+ end
40
+
41
+ def json_schema
42
+ props = {}
43
+ required = []
44
+ params.each do |p|
45
+ entry = { type: TYPE_MAP.fetch(p[:type]) }
46
+ entry[:description] = p[:description] if p[:description]
47
+ entry[:default] = p[:default] unless p[:default].nil?
48
+ entry[:items] = stringify_type(p[:items]) if p[:items]
49
+ entry[:enum] = p[:values] if p[:type] == :enum
50
+ entry[:properties] = p[:properties] if p[:properties]
51
+ props[p[:name]] = entry
52
+ required << p[:name].to_s if p[:required]
53
+ end
54
+
55
+ if params.empty?
56
+ warn_once_no_dsl!
57
+ return {
58
+ name: tool_name,
59
+ description: description,
60
+ parameters: { type: 'object', additionalProperties: true }
61
+ }
62
+ end
63
+
64
+ {
65
+ name: tool_name,
66
+ description: description,
67
+ parameters: {
68
+ type: 'object',
69
+ properties: props,
70
+ required: required
71
+ }
72
+ }
73
+ end
74
+
75
+ private
76
+
77
+ def name_default
78
+ raw = name.to_s.split('::').last
79
+ return '' if raw.nil? || raw.empty?
80
+
81
+ raw.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
82
+ end
83
+
84
+ def stringify_type(hash)
85
+ return hash unless hash.is_a?(Hash) && hash[:type].is_a?(Symbol)
86
+
87
+ hash.merge(type: TYPE_MAP.fetch(hash[:type]))
88
+ end
89
+
90
+ def warn_once_no_dsl!
91
+ return if @warned_no_dsl
92
+
93
+ @warned_no_dsl = true
94
+ Kernel.warn "[rcrewai] Tool #{name} has no DSL declarations; using permissive schema. Declare tool_name/description/param to opt in to a strict JSON schema."
95
+ end
96
+ end
97
+ end