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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +108 -0
- data/LICENSE +21 -0
- data/README.md +328 -0
- data/Rakefile +130 -0
- data/bin/rcrewai +7 -0
- data/docs/_config.yml +59 -0
- data/docs/_layouts/api.html +16 -0
- data/docs/_layouts/default.html +78 -0
- data/docs/_layouts/example.html +24 -0
- data/docs/_layouts/tutorial.html +33 -0
- data/docs/api/configuration.md +327 -0
- data/docs/api/crew.md +345 -0
- data/docs/api/index.md +41 -0
- data/docs/api/tools.md +412 -0
- data/docs/assets/css/style.css +416 -0
- data/docs/examples/human-in-the-loop.md +382 -0
- data/docs/examples/index.md +78 -0
- data/docs/examples/production-ready-crew.md +485 -0
- data/docs/examples/simple-research-crew.md +297 -0
- data/docs/index.md +353 -0
- data/docs/tutorials/getting-started.md +341 -0
- data/examples/async_execution_example.rb +294 -0
- data/examples/hierarchical_crew_example.rb +193 -0
- data/examples/human_in_the_loop_example.rb +233 -0
- data/lib/rcrewai/agent.rb +636 -0
- data/lib/rcrewai/async_executor.rb +248 -0
- data/lib/rcrewai/cli.rb +39 -0
- data/lib/rcrewai/configuration.rb +100 -0
- data/lib/rcrewai/crew.rb +292 -0
- data/lib/rcrewai/human_input.rb +520 -0
- data/lib/rcrewai/llm_client.rb +41 -0
- data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
- data/lib/rcrewai/llm_clients/azure.rb +158 -0
- data/lib/rcrewai/llm_clients/base.rb +82 -0
- data/lib/rcrewai/llm_clients/google.rb +158 -0
- data/lib/rcrewai/llm_clients/ollama.rb +199 -0
- data/lib/rcrewai/llm_clients/openai.rb +124 -0
- data/lib/rcrewai/memory.rb +194 -0
- data/lib/rcrewai/process.rb +421 -0
- data/lib/rcrewai/task.rb +376 -0
- data/lib/rcrewai/tools/base.rb +82 -0
- data/lib/rcrewai/tools/code_executor.rb +333 -0
- data/lib/rcrewai/tools/email_sender.rb +210 -0
- data/lib/rcrewai/tools/file_reader.rb +111 -0
- data/lib/rcrewai/tools/file_writer.rb +115 -0
- data/lib/rcrewai/tools/pdf_processor.rb +342 -0
- data/lib/rcrewai/tools/sql_database.rb +226 -0
- data/lib/rcrewai/tools/web_search.rb +131 -0
- data/lib/rcrewai/version.rb +5 -0
- data/lib/rcrewai.rb +36 -0
- data/rcrewai.gemspec +54 -0
- 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
|
data/lib/rcrewai/cli.rb
ADDED
@@ -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
|
data/lib/rcrewai/crew.rb
ADDED
@@ -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
|