rcrewai 0.2.1 → 0.4.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +64 -1
  5. data/README.md +170 -2
  6. data/ROADMAP.md +84 -0
  7. data/Rakefile +53 -53
  8. data/bin/rcrewai +3 -3
  9. data/docs/mcp.md +109 -0
  10. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  11. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  12. data/docs/upgrading-to-0.3.md +163 -0
  13. data/examples/async_execution_example.rb +82 -81
  14. data/examples/hierarchical_crew_example.rb +68 -72
  15. data/examples/human_in_the_loop_example.rb +73 -74
  16. data/examples/mcp_example.rb +48 -0
  17. data/examples/native_tools_example.rb +64 -0
  18. data/examples/streaming_example.rb +56 -0
  19. data/lib/rcrewai/agent.rb +181 -286
  20. data/lib/rcrewai/async_executor.rb +43 -43
  21. data/lib/rcrewai/cli.rb +11 -11
  22. data/lib/rcrewai/configuration.rb +34 -9
  23. data/lib/rcrewai/crew.rb +134 -39
  24. data/lib/rcrewai/events.rb +30 -0
  25. data/lib/rcrewai/flow/state.rb +47 -0
  26. data/lib/rcrewai/flow/state_store.rb +50 -0
  27. data/lib/rcrewai/flow.rb +243 -0
  28. data/lib/rcrewai/human_input.rb +104 -114
  29. data/lib/rcrewai/knowledge/base.rb +52 -0
  30. data/lib/rcrewai/knowledge/chunker.rb +31 -0
  31. data/lib/rcrewai/knowledge/embedder.rb +48 -0
  32. data/lib/rcrewai/knowledge/sources.rb +83 -0
  33. data/lib/rcrewai/knowledge/store.rb +58 -0
  34. data/lib/rcrewai/knowledge.rb +13 -0
  35. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  36. data/lib/rcrewai/llm_client.rb +24 -1
  37. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  38. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  39. data/lib/rcrewai/llm_clients/base.rb +11 -7
  40. data/lib/rcrewai/llm_clients/google.rb +159 -95
  41. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  42. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  43. data/lib/rcrewai/mcp/client.rb +101 -0
  44. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  45. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  46. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  47. data/lib/rcrewai/mcp.rb +8 -0
  48. data/lib/rcrewai/memory.rb +45 -37
  49. data/lib/rcrewai/output_schema.rb +79 -0
  50. data/lib/rcrewai/planning.rb +65 -0
  51. data/lib/rcrewai/pricing.rb +34 -0
  52. data/lib/rcrewai/process.rb +86 -95
  53. data/lib/rcrewai/provider_schema.rb +38 -0
  54. data/lib/rcrewai/sse_parser.rb +55 -0
  55. data/lib/rcrewai/task.rb +145 -66
  56. data/lib/rcrewai/tool_runner.rb +132 -0
  57. data/lib/rcrewai/tool_schema.rb +97 -0
  58. data/lib/rcrewai/tools/base.rb +98 -37
  59. data/lib/rcrewai/tools/code_executor.rb +71 -74
  60. data/lib/rcrewai/tools/email_sender.rb +70 -78
  61. data/lib/rcrewai/tools/file_reader.rb +38 -30
  62. data/lib/rcrewai/tools/file_writer.rb +40 -38
  63. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  64. data/lib/rcrewai/tools/sql_database.rb +58 -55
  65. data/lib/rcrewai/tools/web_search.rb +26 -25
  66. data/lib/rcrewai/version.rb +2 -2
  67. data/lib/rcrewai.rb +20 -10
  68. data/rcrewai.gemspec +39 -39
  69. metadata +77 -47
@@ -12,7 +12,7 @@ module RCrewAI
12
12
  @timeout = options.fetch(:timeout, 300) # 5 minutes default
13
13
  @logger = Logger.new($stdout)
14
14
  @logger.level = options.fetch(:verbose, false) ? Logger::DEBUG : Logger::INFO
15
-
15
+
16
16
  # Create thread pool for task execution
17
17
  @thread_pool = Concurrent::ThreadPoolExecutor.new(
18
18
  min_threads: 1,
@@ -20,7 +20,7 @@ module RCrewAI
20
20
  max_queue: @max_concurrency * 2,
21
21
  fallback_policy: :caller_runs
22
22
  )
23
-
23
+
24
24
  @futures = {}
25
25
  @task_dependencies = {}
26
26
  @completed_tasks = Concurrent::Set.new
@@ -29,30 +29,30 @@ module RCrewAI
29
29
 
30
30
  def execute_tasks_async(tasks, dependency_graph = {})
31
31
  @logger.info "Starting async execution of #{tasks.length} tasks with max #{@max_concurrency} concurrent threads"
32
-
32
+
33
33
  @task_dependencies = dependency_graph
34
34
  execution_phases = organize_tasks_by_dependencies(tasks)
35
-
35
+
36
36
  start_time = Time.now
37
37
  results = []
38
-
38
+
39
39
  execution_phases.each_with_index do |phase_tasks, phase_index|
40
40
  @logger.info "Executing phase #{phase_index + 1}: #{phase_tasks.length} tasks"
41
-
41
+
42
42
  phase_results = execute_phase_concurrently(phase_tasks, phase_index + 1)
43
43
  results.concat(phase_results)
44
-
44
+
45
45
  # Check if we should continue
46
46
  failed_in_phase = phase_results.count { |r| r[:status] == :failed }
47
- if failed_in_phase > 0 && should_abort_after_failures?(failed_in_phase, phase_tasks.length)
47
+ if failed_in_phase.positive? && should_abort_after_failures?(failed_in_phase, phase_tasks.length)
48
48
  @logger.error "Aborting execution due to #{failed_in_phase} failures in phase #{phase_index + 1}"
49
49
  break
50
50
  end
51
51
  end
52
-
52
+
53
53
  total_time = Time.now - start_time
54
54
  @logger.info "Async execution completed in #{total_time.round(2)}s"
55
-
55
+
56
56
  format_async_results(results, total_time)
57
57
  end
58
58
 
@@ -60,38 +60,36 @@ module RCrewAI
60
60
  future = Concurrent::Future.execute(executor: @thread_pool) do
61
61
  execute_task_with_monitoring(task)
62
62
  end
63
-
63
+
64
64
  @futures[task] = future
65
65
  future
66
66
  end
67
67
 
68
68
  def wait_for_completion(futures, timeout = nil)
69
69
  timeout ||= @timeout
70
-
70
+
71
71
  results = []
72
72
  futures.each do |task, future|
73
- begin
74
- result = future.value(timeout)
75
- results << { task: task, result: result, status: :completed }
76
- rescue Concurrent::TimeoutError
77
- @logger.error "Task #{task.name} timed out after #{timeout}s"
78
- results << { task: task, result: "Task timed out", status: :timeout }
79
- rescue => e
80
- @logger.error "Task #{task.name} failed: #{e.message}"
81
- results << { task: task, result: e.message, status: :failed }
82
- end
73
+ result = future.value(timeout)
74
+ results << { task: task, result: result, status: :completed }
75
+ rescue Concurrent::TimeoutError
76
+ @logger.error "Task #{task.name} timed out after #{timeout}s"
77
+ results << { task: task, result: 'Task timed out', status: :timeout }
78
+ rescue StandardError => e
79
+ @logger.error "Task #{task.name} failed: #{e.message}"
80
+ results << { task: task, result: e.message, status: :failed }
83
81
  end
84
-
82
+
85
83
  results
86
84
  end
87
85
 
88
86
  def shutdown
89
- @logger.info "Shutting down async executor..."
87
+ @logger.info 'Shutting down async executor...'
90
88
  @thread_pool.shutdown
91
- unless @thread_pool.wait_for_termination(30)
92
- @logger.warn "Thread pool did not shut down gracefully, forcing shutdown"
93
- @thread_pool.kill
94
- end
89
+ return if @thread_pool.wait_for_termination(30)
90
+
91
+ @logger.warn 'Thread pool did not shut down gracefully, forcing shutdown'
92
+ @thread_pool.kill
95
93
  end
96
94
 
97
95
  def stats
@@ -136,7 +134,7 @@ module RCrewAI
136
134
 
137
135
  def execute_phase_concurrently(phase_tasks, phase_number)
138
136
  @logger.debug "Phase #{phase_number}: Launching #{phase_tasks.length} concurrent tasks"
139
-
137
+
140
138
  # Launch all tasks in this phase concurrently
141
139
  phase_futures = {}
142
140
  phase_tasks.each do |task|
@@ -146,7 +144,7 @@ module RCrewAI
146
144
 
147
145
  # Wait for all tasks in this phase to complete
148
146
  phase_results = wait_for_completion(phase_futures)
149
-
147
+
150
148
  # Update tracking sets
151
149
  phase_results.each do |result|
152
150
  case result[:status]
@@ -156,31 +154,33 @@ module RCrewAI
156
154
  @failed_tasks.add(result[:task])
157
155
  end
158
156
  end
159
-
160
- @logger.info "Phase #{phase_number} completed: #{phase_results.count { |r| r[:status] == :completed }}/#{phase_tasks.length} successful"
161
-
157
+
158
+ @logger.info "Phase #{phase_number} completed: #{phase_results.count do |r|
159
+ r[:status] == :completed
160
+ end}/#{phase_tasks.length} successful"
161
+
162
162
  phase_results.map { |r| r.merge(phase: phase_number) }
163
163
  end
164
164
 
165
165
  def execute_task_with_monitoring(task)
166
166
  thread_id = Thread.current.object_id
167
167
  @logger.debug "Task #{task.name} starting on thread #{thread_id}"
168
-
168
+
169
169
  start_time = Time.now
170
-
170
+
171
171
  begin
172
172
  # Add async context to task
173
173
  task.instance_variable_set(:@async_execution, true)
174
174
  task.instance_variable_set(:@thread_id, thread_id)
175
-
175
+
176
176
  # Execute the task
177
177
  result = task.execute
178
-
178
+
179
179
  execution_time = Time.now - start_time
180
180
  @logger.debug "Task #{task.name} completed in #{execution_time.round(2)}s on thread #{thread_id}"
181
-
181
+
182
182
  result
183
- rescue => e
183
+ rescue StandardError => e
184
184
  execution_time = Time.now - start_time
185
185
  @logger.error "Task #{task.name} failed after #{execution_time.round(2)}s on thread #{thread_id}: #{e.message}"
186
186
  raise e
@@ -189,7 +189,7 @@ module RCrewAI
189
189
 
190
190
  def should_abort_after_failures?(failed_count, total_count)
191
191
  failure_rate = failed_count.to_f / total_count
192
-
192
+
193
193
  # Abort if more than 50% of tasks in a phase fail
194
194
  failure_rate > 0.5
195
195
  end
@@ -198,7 +198,7 @@ module RCrewAI
198
198
  completed = results.count { |r| r[:status] == :completed }
199
199
  failed = results.count { |r| r[:status] == :failed }
200
200
  timed_out = results.count { |r| r[:status] == :timeout }
201
-
201
+
202
202
  {
203
203
  execution_mode: :async,
204
204
  total_time: total_time,
@@ -227,7 +227,7 @@ module RCrewAI
227
227
  module ClassMethods
228
228
  def execute_async(tasks, **options)
229
229
  executor = AsyncExecutor.new(**options)
230
-
230
+
231
231
  begin
232
232
  results = executor.execute_tasks_async(tasks)
233
233
  results
@@ -245,4 +245,4 @@ module RCrewAI
245
245
  @thread_id
246
246
  end
247
247
  end
248
- end
248
+ end
data/lib/rcrewai/cli.rb CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  module RCrewAI
4
4
  class CLI < Thor
5
- desc "new CREW_NAME", "Create a new AI crew"
5
+ desc 'new CREW_NAME', 'Create a new AI crew'
6
6
  def new(crew_name)
7
7
  puts "Creating new crew: #{crew_name}"
8
8
  Crew.create(crew_name)
9
9
  end
10
10
 
11
- desc "run", "Run the AI crew"
12
- option :crew, type: :string, required: true, desc: "Name of the crew to run"
11
+ desc 'run', 'Run the AI crew'
12
+ option :crew, type: :string, required: true, desc: 'Name of the crew to run'
13
13
  def run
14
14
  crew_name = options[:crew]
15
15
  puts "Running crew: #{crew_name}"
@@ -17,23 +17,23 @@ module RCrewAI
17
17
  crew.execute
18
18
  end
19
19
 
20
- desc "list", "List all available crews"
20
+ desc 'list', 'List all available crews'
21
21
  def list
22
- puts "Available crews:"
22
+ puts 'Available crews:'
23
23
  Crew.list.each do |crew|
24
24
  puts " - #{crew}"
25
25
  end
26
26
  end
27
27
 
28
- desc "agent SUBCOMMAND ...ARGS", "Manage agents"
29
- subcommand "agent", Agent::CLI
28
+ desc 'agent SUBCOMMAND ...ARGS', 'Manage agents'
29
+ subcommand 'agent', Agent::CLI
30
30
 
31
- desc "task SUBCOMMAND ...ARGS", "Manage tasks"
32
- subcommand "task", Task::CLI
31
+ desc 'task SUBCOMMAND ...ARGS', 'Manage tasks'
32
+ subcommand 'task', Task::CLI
33
33
 
34
- desc "version", "Show version"
34
+ desc 'version', 'Show version'
35
35
  def version
36
36
  puts "rcrewai version #{RCrewAI::VERSION}"
37
37
  end
38
38
  end
39
- end
39
+ end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module RCrewAI
4
4
  class Configuration
5
- attr_accessor :llm_provider, :api_key, :model, :temperature, :max_tokens, :timeout
6
- attr_accessor :openai_api_key, :anthropic_api_key, :google_api_key, :azure_api_key
7
- attr_accessor :openai_model, :anthropic_model, :google_model, :azure_model
8
- attr_accessor :base_url, :api_version, :deployment_name
5
+ attr_accessor :llm_provider, :api_key, :model, :temperature, :max_tokens, :timeout,
6
+ :openai_api_key, :anthropic_api_key, :google_api_key, :azure_api_key,
7
+ :openai_model, :anthropic_model, :google_model, :azure_model,
8
+ :base_url, :api_version, :deployment_name,
9
+ :pricing, :ollama_native_tools, :log_level
9
10
 
10
11
  def initialize
11
12
  @llm_provider = :openai
@@ -13,13 +14,17 @@ module RCrewAI
13
14
  @temperature = 0.1
14
15
  @max_tokens = 4000
15
16
  @timeout = 120
16
-
17
+
17
18
  # Default models for each provider
18
19
  @openai_model = 'gpt-4'
19
20
  @anthropic_model = 'claude-3-sonnet-20240229'
20
21
  @google_model = 'gemini-pro'
21
22
  @azure_model = 'gpt-4'
22
-
23
+
24
+ @pricing = nil
25
+ @ollama_native_tools = nil
26
+ @log_level = :info
27
+
23
28
  # Load from environment variables
24
29
  load_from_env
25
30
  end
@@ -54,8 +59,28 @@ module RCrewAI
54
59
  end
55
60
  end
56
61
 
62
+ # Returns a copy of this configuration with the given per-agent overrides
63
+ # applied. The original configuration is left untouched, so agents can each
64
+ # target a different provider/model without mutating global state.
65
+ #
66
+ # config.with_overrides(provider: :anthropic, model: 'claude-3-opus-20240229')
67
+ def with_overrides(provider: nil, model: nil, api_key: nil, temperature: nil)
68
+ copy = dup
69
+ copy.llm_provider = provider.to_sym if provider
70
+ target = copy.llm_provider
71
+
72
+ copy.public_send("#{target}_model=", model) if model && copy.respond_to?("#{target}_model=")
73
+ copy.model = model if model
74
+
75
+ copy.public_send("#{target}_api_key=", api_key) if api_key && copy.respond_to?("#{target}_api_key=")
76
+ copy.api_key = api_key if api_key
77
+
78
+ copy.temperature = temperature unless temperature.nil?
79
+ copy
80
+ end
81
+
57
82
  def validate!
58
- raise ConfigurationError, "LLM provider must be set" if @llm_provider.nil?
83
+ raise ConfigurationError, 'LLM provider must be set' if @llm_provider.nil?
59
84
  raise ConfigurationError, "API key must be set for #{@llm_provider}" if api_key.nil? || api_key.empty?
60
85
  raise ConfigurationError, "Model must be set for #{@llm_provider}" if model.nil? || model.empty?
61
86
  end
@@ -75,7 +100,7 @@ module RCrewAI
75
100
  @anthropic_api_key = ENV['ANTHROPIC_API_KEY'] || ENV['CLAUDE_API_KEY']
76
101
  @google_api_key = ENV['GOOGLE_API_KEY'] || ENV['GEMINI_API_KEY']
77
102
  @azure_api_key = ENV['AZURE_OPENAI_API_KEY']
78
-
103
+
79
104
  @api_key = ENV['LLM_API_KEY'] if @api_key.nil?
80
105
  @base_url = ENV['LLM_BASE_URL'] if @base_url.nil?
81
106
  @api_version = ENV['AZURE_API_VERSION'] if @api_version.nil?
@@ -97,4 +122,4 @@ module RCrewAI
97
122
  def self.reset_configuration!
98
123
  @configuration = Configuration.new
99
124
  end
100
- end
125
+ end
data/lib/rcrewai/crew.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require_relative 'process'
4
5
  require_relative 'async_executor'
6
+ require_relative 'events'
7
+ require_relative 'planning'
5
8
 
6
9
  module RCrewAI
7
10
  class Crew
@@ -16,10 +19,20 @@ module RCrewAI
16
19
  @process_type = options.fetch(:process, :sequential)
17
20
  @verbose = options.fetch(:verbose, false)
18
21
  @max_iterations = options.fetch(:max_iterations, 10)
22
+ @planning = options.fetch(:planning, false)
23
+ @planning_llm = options[:planning_llm]
24
+ @planned = false
25
+ @knowledge = build_knowledge(options[:knowledge], options[:knowledge_sources])
19
26
  @process_instance = nil
20
27
  validate_process_type!
21
28
  end
22
29
 
30
+ attr_reader :knowledge, :stream_sink
31
+
32
+ def planning?
33
+ @planning
34
+ end
35
+
23
36
  def add_agent(agent)
24
37
  @agents << agent
25
38
  end
@@ -28,7 +41,15 @@ module RCrewAI
28
41
  @tasks << task
29
42
  end
30
43
 
31
- def execute(async: false, **async_options)
44
+ def execute(async: false, stream: nil, **async_options, &block)
45
+ sinks = []
46
+ sinks << block if block_given?
47
+ Array(stream).each { |s| sinks << s } if stream
48
+ @stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
49
+
50
+ distribute_knowledge if @knowledge
51
+ run_planning_pass if planning?
52
+
32
53
  if async
33
54
  execute_async(**async_options)
34
55
  else
@@ -36,9 +57,38 @@ module RCrewAI
36
57
  end
37
58
  end
38
59
 
60
+ # Runs the crew repeatedly, collecting feedback after each iteration and
61
+ # persisting it to +filename+ as JSON. +feedback+ is a callable
62
+ # ->(iteration, result) { "..." }; it defaults to prompting a human.
63
+ # Mirrors CrewAI's crew.train.
64
+ def train(n_iterations:, filename:, feedback: nil)
65
+ feedback ||= method(:default_training_feedback)
66
+ entries = []
67
+
68
+ (1..n_iterations).each do |iteration|
69
+ result = execute
70
+ note = feedback.call(iteration, result)
71
+ entries << { iteration: iteration, feedback: note }
72
+ end
73
+
74
+ write_training_file(filename, entries)
75
+ { iterations: n_iterations, filename: filename, entries: entries }
76
+ end
77
+
78
+ # Runs the crew repeatedly and scores each run. +scorer+ is a callable
79
+ # ->(result) { Float }; it defaults to the run's success_rate.
80
+ # Mirrors CrewAI's crew.test.
81
+ def test(n_iterations:, scorer: nil, model: nil) # rubocop:disable Lint/UnusedMethodArgument
82
+ scorer ||= ->(result) { result[:success_rate].to_f }
83
+ scores = (1..n_iterations).map { scorer.call(execute) }
84
+ average = scores.empty? ? 0.0 : (scores.sum / scores.length).round(2)
85
+
86
+ { iterations: n_iterations, scores: scores, average_score: average }
87
+ end
88
+
39
89
  def execute_async(**options)
40
90
  puts "Executing crew: #{name} (async #{process_type} process)"
41
-
91
+
42
92
  case process_type
43
93
  when :sequential
44
94
  execute_sequential_async(**options)
@@ -53,13 +103,13 @@ module RCrewAI
53
103
 
54
104
  def execute_sync
55
105
  puts "Executing crew: #{name} (#{process_type} process)"
56
-
106
+
57
107
  # Create appropriate process instance
58
108
  @process_instance = create_process_instance
59
-
109
+
60
110
  # Execute using the process
61
111
  results = @process_instance.execute
62
-
112
+
63
113
  # Return formatted results
64
114
  format_execution_results(results)
65
115
  end
@@ -67,7 +117,7 @@ module RCrewAI
67
117
  def process=(new_process)
68
118
  @process_type = new_process.to_sym
69
119
  validate_process_type!
70
- @process_instance = nil # Reset process instance
120
+ @process_instance = nil # Reset process instance
71
121
  end
72
122
 
73
123
  def self.create(name)
@@ -84,7 +134,7 @@ module RCrewAI
84
134
 
85
135
  def self.list
86
136
  # List all available crews
87
- ["example_crew", "research_crew", "development_crew"]
137
+ %w[example_crew research_crew development_crew]
88
138
  end
89
139
 
90
140
  def save
@@ -94,11 +144,47 @@ module RCrewAI
94
144
 
95
145
  private
96
146
 
147
+ def build_knowledge(knowledge, sources)
148
+ return knowledge if knowledge
149
+ return nil if sources.nil? || sources.empty?
150
+
151
+ Knowledge::Base.new(sources: sources)
152
+ end
153
+
154
+ def distribute_knowledge
155
+ @knowledge.build!
156
+ agents.each { |agent| agent.crew_knowledge = @knowledge if agent.respond_to?(:crew_knowledge=) }
157
+ end
158
+
159
+ def default_training_feedback(iteration, _result)
160
+ require_relative 'human_input'
161
+ response = HumanInput.new.request_input(
162
+ "Feedback for training iteration #{iteration} (press enter to skip):"
163
+ )
164
+ response.is_a?(Hash) ? response[:input].to_s : response.to_s
165
+ end
166
+
167
+ def write_training_file(filename, entries)
168
+ require 'json'
169
+ require 'fileutils'
170
+ FileUtils.mkdir_p(File.dirname(filename))
171
+ File.write(filename, JSON.pretty_generate(entries))
172
+ end
173
+
174
+ def run_planning_pass
175
+ return if @planned
176
+
177
+ logger = Logger.new($stdout)
178
+ logger.level = verbose ? Logger::DEBUG : Logger::INFO
179
+ Planning.new(self, llm: LLMClient.resolve(@planning_llm), logger: logger).plan!
180
+ @planned = true
181
+ end
182
+
97
183
  def validate_process_type!
98
- valid_processes = [:sequential, :hierarchical, :consensual]
99
- unless valid_processes.include?(process_type)
100
- raise ConfigurationError, "Invalid process type: #{process_type}. Valid types: #{valid_processes.join(', ')}"
101
- end
184
+ valid_processes = %i[sequential hierarchical consensual]
185
+ return if valid_processes.include?(process_type)
186
+
187
+ raise ConfigurationError, "Invalid process type: #{process_type}. Valid types: #{valid_processes.join(', ')}"
102
188
  end
103
189
 
104
190
  def create_process_instance
@@ -116,11 +202,11 @@ module RCrewAI
116
202
 
117
203
  def execute_sequential_async(**options)
118
204
  executor = AsyncExecutor.new(**options)
119
-
205
+
120
206
  begin
121
207
  dependency_graph = build_dependency_graph
122
208
  results = executor.execute_tasks_async(tasks, dependency_graph)
123
-
209
+
124
210
  # Add crew metadata
125
211
  results.merge(
126
212
  crew: name,
@@ -134,38 +220,36 @@ module RCrewAI
134
220
  def execute_hierarchical_async(**options)
135
221
  # For hierarchical async, we need to coordinate through the manager
136
222
  manager_agent = find_manager_agent
137
- unless manager_agent
138
- raise Process::ProcessError, "Hierarchical async execution requires a manager agent"
139
- end
223
+ raise Process::ProcessError, 'Hierarchical async execution requires a manager agent' unless manager_agent
140
224
 
141
225
  puts "Manager #{manager_agent.name} coordinating async execution"
142
-
226
+
143
227
  executor = AsyncExecutor.new(**options)
144
-
228
+
145
229
  begin
146
230
  # Build execution phases respecting dependencies
147
231
  dependency_graph = build_dependency_graph
148
232
  phases = organize_tasks_into_phases(tasks, dependency_graph)
149
-
233
+
150
234
  results = []
151
235
  phases.each_with_index do |phase_tasks, phase_index|
152
236
  puts "Manager delegating phase #{phase_index + 1}: #{phase_tasks.length} tasks"
153
-
237
+
154
238
  # Create delegation contexts for each task
155
239
  phase_tasks.each do |task|
156
240
  unless task.agent
157
241
  # Manager assigns best agent for the task
158
242
  task.instance_variable_set(:@agent, find_best_agent_for_task(task))
159
243
  end
160
-
244
+
161
245
  # Add delegation context
162
246
  add_delegation_context(task, manager_agent)
163
247
  end
164
-
248
+
165
249
  # Execute phase concurrently
166
250
  phase_results = executor.execute_tasks_async(phase_tasks, {})
167
251
  results.concat(phase_results[:results])
168
-
252
+
169
253
  # Check if we should abort
170
254
  failed_count = phase_results[:results].count { |r| r[:status] == :failed }
171
255
  if failed_count > phase_tasks.length * 0.5
@@ -173,7 +257,7 @@ module RCrewAI
173
257
  break
174
258
  end
175
259
  end
176
-
260
+
177
261
  {
178
262
  crew: name,
179
263
  process: :async_hierarchical,
@@ -182,7 +266,13 @@ module RCrewAI
182
266
  completed_tasks: results.count { |r| r[:status] == :completed },
183
267
  failed_tasks: results.count { |r| r[:status] == :failed },
184
268
  results: results,
185
- success_rate: results.empty? ? 0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
269
+ success_rate: if results.empty?
270
+ 0
271
+ else
272
+ (results.count do |r|
273
+ r[:status] == :completed
274
+ end.to_f / results.length * 100).round(1)
275
+ end
186
276
  }
187
277
  ensure
188
278
  executor.shutdown
@@ -192,11 +282,11 @@ module RCrewAI
192
282
  def execute_consensual_async(**options)
193
283
  # Simplified consensual async - execute in parallel with result aggregation
194
284
  executor = AsyncExecutor.new(**options)
195
-
285
+
196
286
  begin
197
287
  # All tasks can potentially run in parallel for consensual
198
288
  results = executor.execute_tasks_async(tasks, {})
199
-
289
+
200
290
  results.merge(
201
291
  crew: name,
202
292
  process: :async_consensual
@@ -239,30 +329,29 @@ module RCrewAI
239
329
  end
240
330
 
241
331
  def find_manager_agent
242
- agents.find { |agent| agent.is_manager? } ||
243
- agents.find { |agent| agent.allow_delegation }
332
+ agents.find(&:is_manager?) ||
333
+ agents.find(&:allow_delegation)
244
334
  end
245
335
 
246
336
  def find_best_agent_for_task(task)
247
337
  # Simple heuristic: match keywords
248
338
  task_keywords = extract_keywords(task.description.downcase)
249
-
339
+
250
340
  # Filter out manager agents first
251
341
  non_manager_agents = agents.reject(&:is_manager?)
252
342
  return nil if non_manager_agents.empty?
253
-
254
- best_agent = non_manager_agents.max_by do |agent|
343
+
344
+ non_manager_agents.max_by do |agent|
255
345
  agent_keywords = extract_keywords("#{agent.role} #{agent.goal}".downcase)
256
346
  common_keywords = (task_keywords & agent_keywords).length
257
347
  tool_bonus = agent.tools.any? ? 0.5 : 0
258
348
  common_keywords + tool_bonus
259
349
  end
260
-
261
- best_agent
262
350
  end
263
351
 
264
352
  def extract_keywords(text)
265
- 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]
353
+ 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
354
+ did will would could should]
266
355
  text.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
267
356
  end
268
357
 
@@ -270,10 +359,10 @@ module RCrewAI
270
359
  delegation_context = {
271
360
  manager: manager_agent.name,
272
361
  delegation_reason: "Assigned by #{manager_agent.role} for optimal task execution",
273
- coordination_notes: "Part of async hierarchical execution",
362
+ coordination_notes: 'Part of async hierarchical execution',
274
363
  async_execution: true
275
364
  }
276
-
365
+
277
366
  task.instance_variable_set(:@delegation_context, delegation_context)
278
367
  end
279
368
 
@@ -285,8 +374,14 @@ module RCrewAI
285
374
  completed_tasks: results.count { |r| r[:status] == :completed },
286
375
  failed_tasks: results.count { |r| r[:status] == :failed },
287
376
  results: results,
288
- success_rate: results.empty? ? 0.0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
377
+ success_rate: if results.empty?
378
+ 0.0
379
+ else
380
+ (results.count do |r|
381
+ r[:status] == :completed
382
+ end.to_f / results.length * 100).round(1)
383
+ end
289
384
  }
290
385
  end
291
386
  end
292
- end
387
+ end