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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Pricing
5
+ # Prices in USD per 1M tokens. List prices as of 2026-05; users can override.
6
+ DEFAULT_PRICES = {
7
+ # OpenAI
8
+ 'gpt-4o' => { input: 2.50, output: 10.00 },
9
+ 'gpt-4o-mini' => { input: 0.15, output: 0.60 },
10
+ 'gpt-4-turbo' => { input: 10.00, output: 30.00 },
11
+ 'gpt-4' => { input: 30.00, output: 60.00 },
12
+ 'gpt-3.5-turbo' => { input: 0.50, output: 1.50 },
13
+ # Anthropic
14
+ 'claude-opus-4-7' => { input: 15.00, output: 75.00 },
15
+ 'claude-sonnet-4-6' => { input: 3.00, output: 15.00 },
16
+ 'claude-haiku-4-5' => { input: 0.80, output: 4.00 },
17
+ 'claude-3-5-sonnet-20241022' => { input: 3.00, output: 15.00 },
18
+ 'claude-3-haiku-20240307' => { input: 0.25, output: 1.25 },
19
+ # Google
20
+ 'gemini-1.5-pro' => { input: 1.25, output: 5.00 },
21
+ 'gemini-1.5-flash' => { input: 0.075, output: 0.30 }
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ def cost_for(model, prompt_tokens:, completion_tokens:)
27
+ table = RCrewAI.configuration.pricing || {}
28
+ entry = table[model] || DEFAULT_PRICES[model]
29
+ return nil unless entry
30
+
31
+ ((prompt_tokens * entry[:input]) + (completion_tokens * entry[:output])) / 1_000_000.0
32
+ end
33
+ end
34
+ end
@@ -12,7 +12,7 @@ module RCrewAI
12
12
  end
13
13
 
14
14
  def execute
15
- raise NotImplementedError, "Subclasses must implement #execute method"
15
+ raise NotImplementedError, 'Subclasses must implement #execute method'
16
16
  end
17
17
 
18
18
  protected
@@ -38,7 +38,7 @@ module RCrewAI
38
38
  begin
39
39
  result = task.execute
40
40
  results << { task: task, result: result, status: :completed }
41
- rescue => e
41
+ rescue StandardError => e
42
42
  @logger.error "Task #{task.name} failed: #{e.message}"
43
43
  results << { task: task, result: e.message, status: :failed }
44
44
  end
@@ -75,14 +75,14 @@ module RCrewAI
75
75
 
76
76
  def find_or_create_manager
77
77
  # Look for an agent marked as manager
78
- manager = crew.agents.find { |agent| agent.is_manager? }
79
-
78
+ manager = crew.agents.find(&:is_manager?)
79
+
80
80
  # If no explicit manager, find agent with delegation capabilities
81
- manager ||= crew.agents.find { |agent| agent.allow_delegation }
82
-
81
+ manager ||= crew.agents.find(&:allow_delegation)
82
+
83
83
  # Create a default manager if none found
84
84
  if manager.nil?
85
- @logger.warn "No manager agent found, creating default manager"
85
+ @logger.warn 'No manager agent found, creating default manager'
86
86
  manager = create_default_manager
87
87
  crew.add_agent(manager)
88
88
  end
@@ -92,10 +92,10 @@ module RCrewAI
92
92
 
93
93
  def create_default_manager
94
94
  RCrewAI::Agent.new(
95
- name: "crew_manager",
96
- role: "Crew Manager",
97
- goal: "Coordinate team efforts and delegate tasks effectively",
98
- backstory: "You are an experienced project manager who coordinates team efforts, delegates tasks appropriately, and ensures deliverables meet requirements.",
95
+ name: 'crew_manager',
96
+ role: 'Crew Manager',
97
+ goal: 'Coordinate team efforts and delegate tasks effectively',
98
+ backstory: 'You are an experienced project manager who coordinates team efforts, delegates tasks appropriately, and ensures deliverables meet requirements.',
99
99
  allow_delegation: true,
100
100
  manager: true,
101
101
  verbose: crew.verbose
@@ -127,7 +127,7 @@ module RCrewAI
127
127
  def find_best_agent_for_task(task, available_agents)
128
128
  # Simple heuristic: match task keywords with agent role/goal
129
129
  task_keywords = extract_keywords(task.description.downcase)
130
-
130
+
131
131
  best_agent = available_agents.max_by do |agent|
132
132
  agent_keywords = extract_keywords("#{agent.role} #{agent.goal}".downcase)
133
133
  common_keywords = (task_keywords & agent_keywords).length
@@ -140,14 +140,15 @@ module RCrewAI
140
140
  end
141
141
 
142
142
  def extract_keywords(text)
143
- stopwords = %w[the a an and or but in on at to for of with by is are was were be been being have has had do does did will would could should]
143
+ stopwords = %w[the a an and or but in on at to for of with by is are was were be been being have has had do
144
+ does did will would could should]
144
145
  text.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
145
146
  end
146
147
 
147
148
  def validate_hierarchy!
148
- raise ProcessError, "No manager agent available" unless manager_agent
149
- raise ProcessError, "No subordinate agents available" if hierarchy[:subordinates].empty?
150
- raise ProcessError, "No tasks to execute" if crew.tasks.empty?
149
+ raise ProcessError, 'No manager agent available' unless manager_agent
150
+ raise ProcessError, 'No subordinate agents available' if hierarchy[:subordinates].empty?
151
+ raise ProcessError, 'No tasks to execute' if crew.tasks.empty?
151
152
  end
152
153
 
153
154
  def create_execution_plan
@@ -191,7 +192,7 @@ module RCrewAI
191
192
 
192
193
  if ready_tasks.empty?
193
194
  # Circular dependency or other issue
194
- @logger.warn "Circular dependency detected, executing remaining tasks in order"
195
+ @logger.warn 'Circular dependency detected, executing remaining tasks in order'
195
196
  phases << remaining_tasks
196
197
  break
197
198
  end
@@ -206,24 +207,24 @@ module RCrewAI
206
207
 
207
208
  def execute_with_manager(plan)
208
209
  results = []
209
-
210
+
210
211
  plan[:phases].each_with_index do |phase_tasks, phase_index|
211
212
  @logger.info "Executing phase #{phase_index + 1}: #{phase_tasks.length} tasks"
212
-
213
+
213
214
  # Manager delegates tasks in this phase
214
215
  phase_results = execute_phase(phase_tasks, phase_index + 1)
215
216
  results.concat(phase_results)
216
-
217
+
217
218
  # Check if we should continue to next phase
218
- if phase_results.any? { |r| r[:status] == :failed }
219
- failed_tasks = phase_results.select { |r| r[:status] == :failed }
220
- @logger.warn "Phase #{phase_index + 1} had #{failed_tasks.length} failures"
221
-
222
- # Manager decides whether to continue
223
- if should_abort_execution?(failed_tasks, phase_index + 1, plan)
224
- @logger.error "Manager decided to abort execution due to critical failures"
225
- break
226
- end
219
+ next unless phase_results.any? { |r| r[:status] == :failed }
220
+
221
+ failed_tasks = phase_results.select { |r| r[:status] == :failed }
222
+ @logger.warn "Phase #{phase_index + 1} had #{failed_tasks.length} failures"
223
+
224
+ # Manager decides whether to continue
225
+ if should_abort_execution?(failed_tasks, phase_index + 1, plan)
226
+ @logger.error 'Manager decided to abort execution due to critical failures'
227
+ break
227
228
  end
228
229
  end
229
230
 
@@ -232,39 +233,38 @@ module RCrewAI
232
233
 
233
234
  def execute_phase(tasks, phase_number)
234
235
  phase_results = []
235
-
236
+
236
237
  # Manager creates delegation plan for this phase
237
238
  delegation_plan = create_delegation_plan(tasks, phase_number)
238
-
239
+
239
240
  # Execute delegated tasks
240
241
  tasks.each do |task|
241
242
  assigned_agent = hierarchy[:task_assignments][task]
242
-
243
+
243
244
  @logger.info "Manager delegating '#{task.name}' to #{assigned_agent.name}"
244
-
245
+
245
246
  begin
246
247
  # Manager provides delegation context
247
248
  delegation_context = create_delegation_context(task, assigned_agent, delegation_plan)
248
-
249
+
249
250
  # Execute task with delegation
250
251
  result = execute_delegated_task(task, assigned_agent, delegation_context)
251
-
252
- phase_results << {
253
- task: task,
254
- result: result,
252
+
253
+ phase_results << {
254
+ task: task,
255
+ result: result,
255
256
  status: :completed,
256
257
  assigned_agent: assigned_agent,
257
258
  phase: phase_number
258
259
  }
259
-
260
+
260
261
  @logger.info "Task '#{task.name}' completed successfully"
261
-
262
- rescue => e
262
+ rescue StandardError => e
263
263
  @logger.error "Delegated task '#{task.name}' failed: #{e.message}"
264
-
265
- phase_results << {
266
- task: task,
267
- result: e.message,
264
+
265
+ phase_results << {
266
+ task: task,
267
+ result: e.message,
268
268
  status: :failed,
269
269
  assigned_agent: assigned_agent,
270
270
  phase: phase_number,
@@ -272,7 +272,7 @@ module RCrewAI
272
272
  }
273
273
  end
274
274
  end
275
-
275
+
276
276
  phase_results
277
277
  end
278
278
 
@@ -288,42 +288,34 @@ module RCrewAI
288
288
  def assign_task_priorities(tasks)
289
289
  # Simple priority assignment based on dependencies and complexity
290
290
  priorities = {}
291
-
291
+
292
292
  tasks.each do |task|
293
293
  priority = :normal
294
-
294
+
295
295
  # High priority if other tasks depend on this one
296
- if crew.tasks.any? { |t| t.context&.include?(task) }
297
- priority = :high
298
- end
299
-
296
+ priority = :high if crew.tasks.any? { |t| t.context&.include?(task) }
297
+
300
298
  # Low priority if task is optional or has many dependencies
301
- if task.context&.length.to_i > 2
302
- priority = :low
303
- end
304
-
299
+ priority = :low if task.context&.length.to_i > 2
300
+
305
301
  priorities[task] = priority
306
302
  end
307
-
303
+
308
304
  priorities
309
305
  end
310
306
 
311
307
  def generate_coordination_notes(tasks)
312
308
  notes = []
313
-
314
- if tasks.length > 1
315
- notes << "Multiple tasks in this phase - coordinate timing if needed"
316
- end
317
-
309
+
310
+ notes << 'Multiple tasks in this phase - coordinate timing if needed' if tasks.length > 1
311
+
318
312
  if tasks.any? { |t| t.context&.any? }
319
- notes << "Some tasks depend on previous results - ensure context is available"
313
+ notes << 'Some tasks depend on previous results - ensure context is available'
320
314
  end
321
-
322
- if tasks.any? { |t| t.tools&.any? }
323
- notes << "Tasks require external tools - monitor for failures"
324
- end
325
-
326
- notes.join(". ") + "."
315
+
316
+ notes << 'Tasks require external tools - monitor for failures' if tasks.any? { |t| t.tools&.any? }
317
+
318
+ "#{notes.join('. ')}."
327
319
  end
328
320
 
329
321
  def create_delegation_context(task, assigned_agent, delegation_plan)
@@ -332,51 +324,51 @@ module RCrewAI
332
324
  assignment_reason: generate_assignment_reason(task, assigned_agent),
333
325
  phase_context: delegation_plan,
334
326
  expectations: generate_task_expectations(task),
335
- escalation_notes: "Contact manager if issues arise or guidance needed"
327
+ escalation_notes: 'Contact manager if issues arise or guidance needed'
336
328
  }
337
329
  end
338
330
 
339
- def generate_assignment_reason(task, agent)
331
+ def generate_assignment_reason(_task, agent)
340
332
  "Assigned to #{agent.name} based on role '#{agent.role}' and expertise alignment with task requirements"
341
333
  end
342
334
 
343
335
  def generate_task_expectations(task)
344
336
  expectations = []
345
337
  expectations << "Expected output: #{task.expected_output}" if task.expected_output
346
- expectations << "Quality: Professional and thorough"
347
- expectations << "Communication: Report progress and any blockers"
348
- expectations.join(". ") + "."
338
+ expectations << 'Quality: Professional and thorough'
339
+ expectations << 'Communication: Report progress and any blockers'
340
+ "#{expectations.join('. ')}."
349
341
  end
350
342
 
351
343
  def execute_delegated_task(task, agent, delegation_context)
352
344
  # Enhance task with delegation context
353
345
  enhanced_task = task.dup
354
346
  enhanced_task.instance_variable_set(:@delegation_context, delegation_context)
355
-
347
+
356
348
  # Define method to access delegation context
357
349
  def enhanced_task.delegation_context
358
350
  @delegation_context
359
351
  end
360
-
352
+
361
353
  # Execute the task
362
354
  agent.execute_task(enhanced_task)
363
355
  end
364
356
 
365
- def should_abort_execution?(failed_tasks, phase_number, plan)
357
+ def should_abort_execution?(failed_tasks, phase_number, _plan)
366
358
  # Abort if more than 50% of critical tasks failed
367
359
  critical_failures = failed_tasks.count { |r| r[:task].context&.any? || r[:task].expected_output }
368
-
360
+
369
361
  if critical_failures > (failed_tasks.length * 0.5)
370
362
  @logger.error "Too many critical task failures (#{critical_failures}/#{failed_tasks.length})"
371
363
  return true
372
364
  end
373
-
365
+
374
366
  # Abort if we're early in execution and having major issues
375
367
  if phase_number <= 2 && failed_tasks.length > 1
376
- @logger.error "Multiple failures in early phases indicate systemic issues"
368
+ @logger.error 'Multiple failures in early phases indicate systemic issues'
377
369
  return true
378
370
  end
379
-
371
+
380
372
  false
381
373
  end
382
374
  end
@@ -384,38 +376,37 @@ module RCrewAI
384
376
  class Consensual < Base
385
377
  def execute
386
378
  log_execution_start
387
- @logger.info "Consensual execution - agents collaborate on decisions"
388
-
379
+ @logger.info 'Consensual execution - agents collaborate on decisions'
380
+
389
381
  # For now, implement as enhanced sequential with collaboration
390
382
  # Full consensual process would involve agent voting/discussion
391
383
  results = []
392
-
384
+
393
385
  crew.tasks.each do |task|
394
386
  @logger.info "Collaborative execution of task: #{task.name}"
395
-
387
+
396
388
  # Simple consensus: let multiple agents provide input
397
389
  consensus_result = execute_with_consensus(task)
398
390
  results << consensus_result
399
391
  end
400
-
392
+
401
393
  log_execution_end(results)
402
394
  results
403
395
  end
404
-
396
+
405
397
  private
406
-
398
+
407
399
  def execute_with_consensus(task)
408
400
  # For now, just execute normally
409
401
  # Future: implement actual consensus mechanisms
410
- begin
411
- result = task.execute
412
- { task: task, result: result, status: :completed }
413
- rescue => e
414
- { task: task, result: e.message, status: :failed }
415
- end
402
+
403
+ result = task.execute
404
+ { task: task, result: result, status: :completed }
405
+ rescue StandardError => e
406
+ { task: task, result: e.message, status: :failed }
416
407
  end
417
408
  end
418
409
 
419
410
  class ProcessError < RCrewAI::Error; end
420
411
  end
421
- end
412
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module ProviderSchema
5
+ module_function
6
+
7
+ def for(provider, canonical)
8
+ case provider.to_sym
9
+ when :openai, :azure, :ollama
10
+ { type: 'function', function: canonical }
11
+ when :anthropic
12
+ {
13
+ name: canonical[:name],
14
+ description: canonical[:description],
15
+ input_schema: canonical[:parameters]
16
+ }
17
+ when :google
18
+ {
19
+ function_declarations: [{
20
+ name: canonical[:name],
21
+ description: canonical[:description],
22
+ parameters: canonical[:parameters]
23
+ }]
24
+ }
25
+ else
26
+ raise ArgumentError, "unknown provider #{provider.inspect}"
27
+ end
28
+ end
29
+
30
+ def for_many(provider, canonicals)
31
+ if provider.to_sym == :google
32
+ { function_declarations: canonicals.map { |c| self.for(:google, c)[:function_declarations].first } }
33
+ else
34
+ canonicals.map { |c| self.for(provider, c) }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ # Minimal Server-Sent Events line parser.
5
+ # Supports LF and CRLF line terminators (sufficient for OpenAI, Anthropic,
6
+ # Google, and well-behaved MCP HTTP servers). Lone-CR terminators are NOT
7
+ # handled — see https://html.spec.whatwg.org/multipage/server-sent-events.html
8
+ # if that becomes a requirement.
9
+ # Feed bytes via #feed(chunk); yields { event: String, data: String } per complete event.
10
+ class SSEParser
11
+ def initialize(&block)
12
+ @on_event = block
13
+ @buffer = String.new(encoding: Encoding::UTF_8)
14
+ @event = 'message'
15
+ @data_lines = []
16
+ end
17
+
18
+ def feed(chunk)
19
+ chunk = chunk.dup.force_encoding(Encoding::UTF_8) unless chunk.encoding == Encoding::UTF_8
20
+ @buffer << chunk
21
+ while (idx = @buffer.index("\n"))
22
+ line = @buffer.slice!(0, idx + 1).chomp
23
+ if line.empty?
24
+ dispatch
25
+ elsif line.start_with?(':')
26
+ # comment line, ignore
27
+ elsif (colon = line.index(':'))
28
+ field = line[0...colon]
29
+ value = line[(colon + 1)..]
30
+ value = value[1..] if value.start_with?(' ')
31
+ handle_field(field, value)
32
+ else
33
+ handle_field(line, '')
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def handle_field(field, value)
41
+ case field
42
+ when 'event' then @event = value
43
+ when 'data' then @data_lines << value
44
+ end
45
+ end
46
+
47
+ def dispatch
48
+ return if @data_lines.empty?
49
+
50
+ @on_event.call(event: @event, data: @data_lines.join("\n"))
51
+ @event = 'message'
52
+ @data_lines = []
53
+ end
54
+ end
55
+ end