rcrewai 0.1.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +108 -0
  3. data/LICENSE +21 -0
  4. data/README.md +328 -0
  5. data/Rakefile +130 -0
  6. data/bin/rcrewai +7 -0
  7. data/docs/_config.yml +59 -0
  8. data/docs/_layouts/api.html +16 -0
  9. data/docs/_layouts/default.html +78 -0
  10. data/docs/_layouts/example.html +24 -0
  11. data/docs/_layouts/tutorial.html +33 -0
  12. data/docs/api/configuration.md +327 -0
  13. data/docs/api/crew.md +345 -0
  14. data/docs/api/index.md +41 -0
  15. data/docs/api/tools.md +412 -0
  16. data/docs/assets/css/style.css +416 -0
  17. data/docs/examples/human-in-the-loop.md +382 -0
  18. data/docs/examples/index.md +78 -0
  19. data/docs/examples/production-ready-crew.md +485 -0
  20. data/docs/examples/simple-research-crew.md +297 -0
  21. data/docs/index.md +353 -0
  22. data/docs/tutorials/getting-started.md +341 -0
  23. data/examples/async_execution_example.rb +294 -0
  24. data/examples/hierarchical_crew_example.rb +193 -0
  25. data/examples/human_in_the_loop_example.rb +233 -0
  26. data/lib/rcrewai/agent.rb +636 -0
  27. data/lib/rcrewai/async_executor.rb +248 -0
  28. data/lib/rcrewai/cli.rb +39 -0
  29. data/lib/rcrewai/configuration.rb +100 -0
  30. data/lib/rcrewai/crew.rb +292 -0
  31. data/lib/rcrewai/human_input.rb +520 -0
  32. data/lib/rcrewai/llm_client.rb +41 -0
  33. data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
  34. data/lib/rcrewai/llm_clients/azure.rb +158 -0
  35. data/lib/rcrewai/llm_clients/base.rb +82 -0
  36. data/lib/rcrewai/llm_clients/google.rb +158 -0
  37. data/lib/rcrewai/llm_clients/ollama.rb +199 -0
  38. data/lib/rcrewai/llm_clients/openai.rb +124 -0
  39. data/lib/rcrewai/memory.rb +194 -0
  40. data/lib/rcrewai/process.rb +421 -0
  41. data/lib/rcrewai/task.rb +376 -0
  42. data/lib/rcrewai/tools/base.rb +82 -0
  43. data/lib/rcrewai/tools/code_executor.rb +333 -0
  44. data/lib/rcrewai/tools/email_sender.rb +210 -0
  45. data/lib/rcrewai/tools/file_reader.rb +111 -0
  46. data/lib/rcrewai/tools/file_writer.rb +115 -0
  47. data/lib/rcrewai/tools/pdf_processor.rb +342 -0
  48. data/lib/rcrewai/tools/sql_database.rb +226 -0
  49. data/lib/rcrewai/tools/web_search.rb +131 -0
  50. data/lib/rcrewai/version.rb +5 -0
  51. data/lib/rcrewai.rb +36 -0
  52. data/rcrewai.gemspec +54 -0
  53. metadata +365 -0
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'logger'
5
+
6
+ module RCrewAI
7
+ class AsyncExecutor
8
+ attr_reader :thread_pool, :logger, :max_concurrency
9
+
10
+ def initialize(**options)
11
+ @max_concurrency = options.fetch(:max_concurrency, Concurrent.processor_count)
12
+ @timeout = options.fetch(:timeout, 300) # 5 minutes default
13
+ @logger = Logger.new($stdout)
14
+ @logger.level = options.fetch(:verbose, false) ? Logger::DEBUG : Logger::INFO
15
+
16
+ # Create thread pool for task execution
17
+ @thread_pool = Concurrent::ThreadPoolExecutor.new(
18
+ min_threads: 1,
19
+ max_threads: @max_concurrency,
20
+ max_queue: @max_concurrency * 2,
21
+ fallback_policy: :caller_runs
22
+ )
23
+
24
+ @futures = {}
25
+ @task_dependencies = {}
26
+ @completed_tasks = Concurrent::Set.new
27
+ @failed_tasks = Concurrent::Set.new
28
+ end
29
+
30
+ def execute_tasks_async(tasks, dependency_graph = {})
31
+ @logger.info "Starting async execution of #{tasks.length} tasks with max #{@max_concurrency} concurrent threads"
32
+
33
+ @task_dependencies = dependency_graph
34
+ execution_phases = organize_tasks_by_dependencies(tasks)
35
+
36
+ start_time = Time.now
37
+ results = []
38
+
39
+ execution_phases.each_with_index do |phase_tasks, phase_index|
40
+ @logger.info "Executing phase #{phase_index + 1}: #{phase_tasks.length} tasks"
41
+
42
+ phase_results = execute_phase_concurrently(phase_tasks, phase_index + 1)
43
+ results.concat(phase_results)
44
+
45
+ # Check if we should continue
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)
48
+ @logger.error "Aborting execution due to #{failed_in_phase} failures in phase #{phase_index + 1}"
49
+ break
50
+ end
51
+ end
52
+
53
+ total_time = Time.now - start_time
54
+ @logger.info "Async execution completed in #{total_time.round(2)}s"
55
+
56
+ format_async_results(results, total_time)
57
+ end
58
+
59
+ def execute_single_task_async(task)
60
+ future = Concurrent::Future.execute(executor: @thread_pool) do
61
+ execute_task_with_monitoring(task)
62
+ end
63
+
64
+ @futures[task] = future
65
+ future
66
+ end
67
+
68
+ def wait_for_completion(futures, timeout = nil)
69
+ timeout ||= @timeout
70
+
71
+ results = []
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
83
+ end
84
+
85
+ results
86
+ end
87
+
88
+ def shutdown
89
+ @logger.info "Shutting down async executor..."
90
+ @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
95
+ end
96
+
97
+ def stats
98
+ {
99
+ max_concurrency: @max_concurrency,
100
+ active_threads: @thread_pool.length,
101
+ queue_length: @thread_pool.queue_length,
102
+ completed_task_count: @completed_tasks.size,
103
+ failed_task_count: @failed_tasks.size,
104
+ pool_shutdown: @thread_pool.shutdown?
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def organize_tasks_by_dependencies(tasks)
111
+ phases = []
112
+ remaining_tasks = tasks.dup
113
+ completed_task_names = Set.new
114
+
115
+ while remaining_tasks.any?
116
+ # Find tasks with no unmet dependencies
117
+ ready_tasks = remaining_tasks.select do |task|
118
+ dependencies = @task_dependencies[task] || task.context || []
119
+ dependencies.all? { |dep| completed_task_names.include?(dep.name) }
120
+ end
121
+
122
+ if ready_tasks.empty?
123
+ # Handle circular dependencies by running remaining tasks in parallel
124
+ @logger.warn "Circular dependency detected, executing remaining #{remaining_tasks.length} tasks in parallel"
125
+ phases << remaining_tasks
126
+ break
127
+ end
128
+
129
+ phases << ready_tasks
130
+ remaining_tasks -= ready_tasks
131
+ ready_tasks.each { |task| completed_task_names.add(task.name) }
132
+ end
133
+
134
+ phases
135
+ end
136
+
137
+ def execute_phase_concurrently(phase_tasks, phase_number)
138
+ @logger.debug "Phase #{phase_number}: Launching #{phase_tasks.length} concurrent tasks"
139
+
140
+ # Launch all tasks in this phase concurrently
141
+ phase_futures = {}
142
+ phase_tasks.each do |task|
143
+ future = execute_single_task_async(task)
144
+ phase_futures[task] = future
145
+ end
146
+
147
+ # Wait for all tasks in this phase to complete
148
+ phase_results = wait_for_completion(phase_futures)
149
+
150
+ # Update tracking sets
151
+ phase_results.each do |result|
152
+ case result[:status]
153
+ when :completed
154
+ @completed_tasks.add(result[:task])
155
+ when :failed, :timeout
156
+ @failed_tasks.add(result[:task])
157
+ end
158
+ end
159
+
160
+ @logger.info "Phase #{phase_number} completed: #{phase_results.count { |r| r[:status] == :completed }}/#{phase_tasks.length} successful"
161
+
162
+ phase_results.map { |r| r.merge(phase: phase_number) }
163
+ end
164
+
165
+ def execute_task_with_monitoring(task)
166
+ thread_id = Thread.current.object_id
167
+ @logger.debug "Task #{task.name} starting on thread #{thread_id}"
168
+
169
+ start_time = Time.now
170
+
171
+ begin
172
+ # Add async context to task
173
+ task.instance_variable_set(:@async_execution, true)
174
+ task.instance_variable_set(:@thread_id, thread_id)
175
+
176
+ # Execute the task
177
+ result = task.execute
178
+
179
+ execution_time = Time.now - start_time
180
+ @logger.debug "Task #{task.name} completed in #{execution_time.round(2)}s on thread #{thread_id}"
181
+
182
+ result
183
+ rescue => e
184
+ execution_time = Time.now - start_time
185
+ @logger.error "Task #{task.name} failed after #{execution_time.round(2)}s on thread #{thread_id}: #{e.message}"
186
+ raise e
187
+ end
188
+ end
189
+
190
+ def should_abort_after_failures?(failed_count, total_count)
191
+ failure_rate = failed_count.to_f / total_count
192
+
193
+ # Abort if more than 50% of tasks in a phase fail
194
+ failure_rate > 0.5
195
+ end
196
+
197
+ def format_async_results(results, total_time)
198
+ completed = results.count { |r| r[:status] == :completed }
199
+ failed = results.count { |r| r[:status] == :failed }
200
+ timed_out = results.count { |r| r[:status] == :timeout }
201
+
202
+ {
203
+ execution_mode: :async,
204
+ total_time: total_time,
205
+ max_concurrency: @max_concurrency,
206
+ total_tasks: results.length,
207
+ completed_tasks: completed,
208
+ failed_tasks: failed,
209
+ timed_out_tasks: timed_out,
210
+ success_rate: (completed.to_f / results.length * 100).round(1),
211
+ results: results,
212
+ thread_pool_stats: {
213
+ max_threads: @thread_pool.max_length,
214
+ current_threads: @thread_pool.length,
215
+ largest_length: @thread_pool.largest_length
216
+ }
217
+ }
218
+ end
219
+ end
220
+
221
+ # Extensions to existing classes for async support
222
+ module AsyncExtensions
223
+ def self.included(base)
224
+ base.extend(ClassMethods)
225
+ end
226
+
227
+ module ClassMethods
228
+ def execute_async(tasks, **options)
229
+ executor = AsyncExecutor.new(**options)
230
+
231
+ begin
232
+ results = executor.execute_tasks_async(tasks)
233
+ results
234
+ ensure
235
+ executor.shutdown
236
+ end
237
+ end
238
+ end
239
+
240
+ def async_execution?
241
+ @async_execution || false
242
+ end
243
+
244
+ def thread_id
245
+ @thread_id
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ class CLI < Thor
5
+ desc "new CREW_NAME", "Create a new AI crew"
6
+ def new(crew_name)
7
+ puts "Creating new crew: #{crew_name}"
8
+ Crew.create(crew_name)
9
+ end
10
+
11
+ desc "run", "Run the AI crew"
12
+ option :crew, type: :string, required: true, desc: "Name of the crew to run"
13
+ def run
14
+ crew_name = options[:crew]
15
+ puts "Running crew: #{crew_name}"
16
+ crew = Crew.load(crew_name)
17
+ crew.execute
18
+ end
19
+
20
+ desc "list", "List all available crews"
21
+ def list
22
+ puts "Available crews:"
23
+ Crew.list.each do |crew|
24
+ puts " - #{crew}"
25
+ end
26
+ end
27
+
28
+ desc "agent SUBCOMMAND ...ARGS", "Manage agents"
29
+ subcommand "agent", Agent::CLI
30
+
31
+ desc "task SUBCOMMAND ...ARGS", "Manage tasks"
32
+ subcommand "task", Task::CLI
33
+
34
+ desc "version", "Show version"
35
+ def version
36
+ puts "rcrewai version #{RCrewAI::VERSION}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
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
9
+
10
+ def initialize
11
+ @llm_provider = :openai
12
+ @model = 'gpt-4'
13
+ @temperature = 0.1
14
+ @max_tokens = 4000
15
+ @timeout = 120
16
+
17
+ # Default models for each provider
18
+ @openai_model = 'gpt-4'
19
+ @anthropic_model = 'claude-3-sonnet-20240229'
20
+ @google_model = 'gemini-pro'
21
+ @azure_model = 'gpt-4'
22
+
23
+ # Load from environment variables
24
+ load_from_env
25
+ end
26
+
27
+ def api_key
28
+ case @llm_provider
29
+ when :openai
30
+ @openai_api_key || @api_key
31
+ when :anthropic
32
+ @anthropic_api_key || @api_key
33
+ when :google
34
+ @google_api_key || @api_key
35
+ when :azure
36
+ @azure_api_key || @api_key
37
+ else
38
+ @api_key
39
+ end
40
+ end
41
+
42
+ def model
43
+ case @llm_provider
44
+ when :openai
45
+ @openai_model || @model
46
+ when :anthropic
47
+ @anthropic_model || @model
48
+ when :google
49
+ @google_model || @model
50
+ when :azure
51
+ @azure_model || @model
52
+ else
53
+ @model
54
+ end
55
+ end
56
+
57
+ def validate!
58
+ raise ConfigurationError, "LLM provider must be set" if @llm_provider.nil?
59
+ raise ConfigurationError, "API key must be set for #{@llm_provider}" if api_key.nil? || api_key.empty?
60
+ raise ConfigurationError, "Model must be set for #{@llm_provider}" if model.nil? || model.empty?
61
+ end
62
+
63
+ def supported_providers
64
+ %i[openai anthropic google azure ollama]
65
+ end
66
+
67
+ def provider_supported?(provider)
68
+ supported_providers.include?(provider.to_sym)
69
+ end
70
+
71
+ private
72
+
73
+ def load_from_env
74
+ @openai_api_key = ENV['OPENAI_API_KEY']
75
+ @anthropic_api_key = ENV['ANTHROPIC_API_KEY'] || ENV['CLAUDE_API_KEY']
76
+ @google_api_key = ENV['GOOGLE_API_KEY'] || ENV['GEMINI_API_KEY']
77
+ @azure_api_key = ENV['AZURE_OPENAI_API_KEY']
78
+
79
+ @api_key = ENV['LLM_API_KEY'] if @api_key.nil?
80
+ @base_url = ENV['LLM_BASE_URL'] if @base_url.nil?
81
+ @api_version = ENV['AZURE_API_VERSION'] if @api_version.nil?
82
+ @deployment_name = ENV['AZURE_DEPLOYMENT_NAME'] if @deployment_name.nil?
83
+ end
84
+ end
85
+
86
+ class ConfigurationError < Error; end
87
+
88
+ def self.configuration
89
+ @configuration ||= Configuration.new
90
+ end
91
+
92
+ def self.configure(validate: true)
93
+ yield(configuration)
94
+ configuration.validate! if validate
95
+ end
96
+
97
+ def self.reset_configuration!
98
+ @configuration = Configuration.new
99
+ end
100
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'process'
4
+ require_relative 'async_executor'
5
+
6
+ module RCrewAI
7
+ class Crew
8
+ include AsyncExtensions
9
+ attr_reader :name, :agents, :tasks, :process_type
10
+ attr_accessor :verbose, :max_iterations
11
+
12
+ def initialize(name, **options)
13
+ @name = name
14
+ @agents = []
15
+ @tasks = []
16
+ @process_type = options.fetch(:process, :sequential)
17
+ @verbose = options.fetch(:verbose, false)
18
+ @max_iterations = options.fetch(:max_iterations, 10)
19
+ @process_instance = nil
20
+ validate_process_type!
21
+ end
22
+
23
+ def add_agent(agent)
24
+ @agents << agent
25
+ end
26
+
27
+ def add_task(task)
28
+ @tasks << task
29
+ end
30
+
31
+ def execute(async: false, **async_options)
32
+ if async
33
+ execute_async(**async_options)
34
+ else
35
+ execute_sync
36
+ end
37
+ end
38
+
39
+ def execute_async(**options)
40
+ puts "Executing crew: #{name} (async #{process_type} process)"
41
+
42
+ case process_type
43
+ when :sequential
44
+ execute_sequential_async(**options)
45
+ when :hierarchical
46
+ execute_hierarchical_async(**options)
47
+ when :consensual
48
+ execute_consensual_async(**options)
49
+ else
50
+ raise ConfigurationError, "Async execution not implemented for #{process_type} process"
51
+ end
52
+ end
53
+
54
+ def execute_sync
55
+ puts "Executing crew: #{name} (#{process_type} process)"
56
+
57
+ # Create appropriate process instance
58
+ @process_instance = create_process_instance
59
+
60
+ # Execute using the process
61
+ results = @process_instance.execute
62
+
63
+ # Return formatted results
64
+ format_execution_results(results)
65
+ end
66
+
67
+ def process=(new_process)
68
+ @process_type = new_process.to_sym
69
+ validate_process_type!
70
+ @process_instance = nil # Reset process instance
71
+ end
72
+
73
+ def self.create(name)
74
+ crew = new(name)
75
+ crew.save
76
+ puts "Crew '#{name}' created successfully!"
77
+ crew
78
+ end
79
+
80
+ def self.load(name)
81
+ # Load crew configuration from file
82
+ new(name)
83
+ end
84
+
85
+ def self.list
86
+ # List all available crews
87
+ ["example_crew", "research_crew", "development_crew"]
88
+ end
89
+
90
+ def save
91
+ # Save crew configuration to file
92
+ true
93
+ end
94
+
95
+ private
96
+
97
+ 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
102
+ end
103
+
104
+ def create_process_instance
105
+ case process_type
106
+ when :sequential
107
+ Process::Sequential.new(self)
108
+ when :hierarchical
109
+ Process::Hierarchical.new(self)
110
+ when :consensual
111
+ Process::Consensual.new(self)
112
+ else
113
+ raise ConfigurationError, "Unsupported process type: #{process_type}"
114
+ end
115
+ end
116
+
117
+ def execute_sequential_async(**options)
118
+ executor = AsyncExecutor.new(**options)
119
+
120
+ begin
121
+ dependency_graph = build_dependency_graph
122
+ results = executor.execute_tasks_async(tasks, dependency_graph)
123
+
124
+ # Add crew metadata
125
+ results.merge(
126
+ crew: name,
127
+ process: :async_sequential
128
+ )
129
+ ensure
130
+ executor.shutdown
131
+ end
132
+ end
133
+
134
+ def execute_hierarchical_async(**options)
135
+ # For hierarchical async, we need to coordinate through the manager
136
+ manager_agent = find_manager_agent
137
+ unless manager_agent
138
+ raise Process::ProcessError, "Hierarchical async execution requires a manager agent"
139
+ end
140
+
141
+ puts "Manager #{manager_agent.name} coordinating async execution"
142
+
143
+ executor = AsyncExecutor.new(**options)
144
+
145
+ begin
146
+ # Build execution phases respecting dependencies
147
+ dependency_graph = build_dependency_graph
148
+ phases = organize_tasks_into_phases(tasks, dependency_graph)
149
+
150
+ results = []
151
+ phases.each_with_index do |phase_tasks, phase_index|
152
+ puts "Manager delegating phase #{phase_index + 1}: #{phase_tasks.length} tasks"
153
+
154
+ # Create delegation contexts for each task
155
+ phase_tasks.each do |task|
156
+ unless task.agent
157
+ # Manager assigns best agent for the task
158
+ task.instance_variable_set(:@agent, find_best_agent_for_task(task))
159
+ end
160
+
161
+ # Add delegation context
162
+ add_delegation_context(task, manager_agent)
163
+ end
164
+
165
+ # Execute phase concurrently
166
+ phase_results = executor.execute_tasks_async(phase_tasks, {})
167
+ results.concat(phase_results[:results])
168
+
169
+ # Check if we should abort
170
+ failed_count = phase_results[:results].count { |r| r[:status] == :failed }
171
+ if failed_count > phase_tasks.length * 0.5
172
+ puts "Manager aborting execution due to high failure rate in phase #{phase_index + 1}"
173
+ break
174
+ end
175
+ end
176
+
177
+ {
178
+ crew: name,
179
+ process: :async_hierarchical,
180
+ manager: manager_agent.name,
181
+ total_tasks: tasks.length,
182
+ completed_tasks: results.count { |r| r[:status] == :completed },
183
+ failed_tasks: results.count { |r| r[:status] == :failed },
184
+ results: results,
185
+ success_rate: results.empty? ? 0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
186
+ }
187
+ ensure
188
+ executor.shutdown
189
+ end
190
+ end
191
+
192
+ def execute_consensual_async(**options)
193
+ # Simplified consensual async - execute in parallel with result aggregation
194
+ executor = AsyncExecutor.new(**options)
195
+
196
+ begin
197
+ # All tasks can potentially run in parallel for consensual
198
+ results = executor.execute_tasks_async(tasks, {})
199
+
200
+ results.merge(
201
+ crew: name,
202
+ process: :async_consensual
203
+ )
204
+ ensure
205
+ executor.shutdown
206
+ end
207
+ end
208
+
209
+ def build_dependency_graph
210
+ graph = {}
211
+ tasks.each do |task|
212
+ graph[task] = task.context || []
213
+ end
214
+ graph
215
+ end
216
+
217
+ def organize_tasks_into_phases(tasks, dependency_graph)
218
+ phases = []
219
+ remaining_tasks = tasks.dup
220
+ completed_task_names = Set.new
221
+
222
+ while remaining_tasks.any?
223
+ ready_tasks = remaining_tasks.select do |task|
224
+ dependencies = dependency_graph[task] || []
225
+ dependencies.all? { |dep| completed_task_names.include?(dep.name) }
226
+ end
227
+
228
+ if ready_tasks.empty?
229
+ phases << remaining_tasks
230
+ break
231
+ end
232
+
233
+ phases << ready_tasks
234
+ remaining_tasks -= ready_tasks
235
+ ready_tasks.each { |task| completed_task_names.add(task.name) }
236
+ end
237
+
238
+ phases
239
+ end
240
+
241
+ def find_manager_agent
242
+ agents.find { |agent| agent.is_manager? } ||
243
+ agents.find { |agent| agent.allow_delegation }
244
+ end
245
+
246
+ def find_best_agent_for_task(task)
247
+ # Simple heuristic: match keywords
248
+ task_keywords = extract_keywords(task.description.downcase)
249
+
250
+ # Filter out manager agents first
251
+ non_manager_agents = agents.reject(&:is_manager?)
252
+ return nil if non_manager_agents.empty?
253
+
254
+ best_agent = non_manager_agents.max_by do |agent|
255
+ agent_keywords = extract_keywords("#{agent.role} #{agent.goal}".downcase)
256
+ common_keywords = (task_keywords & agent_keywords).length
257
+ tool_bonus = agent.tools.any? ? 0.5 : 0
258
+ common_keywords + tool_bonus
259
+ end
260
+
261
+ best_agent
262
+ end
263
+
264
+ 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]
266
+ text.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
267
+ end
268
+
269
+ def add_delegation_context(task, manager_agent)
270
+ delegation_context = {
271
+ manager: manager_agent.name,
272
+ delegation_reason: "Assigned by #{manager_agent.role} for optimal task execution",
273
+ coordination_notes: "Part of async hierarchical execution",
274
+ async_execution: true
275
+ }
276
+
277
+ task.instance_variable_set(:@delegation_context, delegation_context)
278
+ end
279
+
280
+ def format_execution_results(results)
281
+ {
282
+ crew: name,
283
+ process: process_type,
284
+ total_tasks: results.length,
285
+ completed_tasks: results.count { |r| r[:status] == :completed },
286
+ failed_tasks: results.count { |r| r[:status] == :failed },
287
+ results: results,
288
+ success_rate: results.empty? ? 0.0 : (results.count { |r| r[:status] == :completed }.to_f / results.length * 100).round(1)
289
+ }
290
+ end
291
+ end
292
+ end