rcrewai 0.2.1 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +99 -0
- data/CHANGELOG.md +24 -0
- data/README.md +2 -2
- data/Rakefile +53 -53
- data/bin/rcrewai +3 -3
- data/docs/mcp.md +109 -0
- data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
- data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
- data/docs/upgrading-to-0.3.md +163 -0
- data/examples/async_execution_example.rb +82 -81
- data/examples/hierarchical_crew_example.rb +68 -72
- data/examples/human_in_the_loop_example.rb +73 -74
- data/examples/mcp_example.rb +48 -0
- data/examples/native_tools_example.rb +64 -0
- data/examples/streaming_example.rb +56 -0
- data/lib/rcrewai/agent.rb +148 -287
- data/lib/rcrewai/async_executor.rb +43 -43
- data/lib/rcrewai/cli.rb +11 -11
- data/lib/rcrewai/configuration.rb +14 -9
- data/lib/rcrewai/crew.rb +56 -39
- data/lib/rcrewai/events.rb +30 -0
- data/lib/rcrewai/human_input.rb +104 -114
- data/lib/rcrewai/legacy_react_runner.rb +172 -0
- data/lib/rcrewai/llm_client.rb +1 -1
- data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
- data/lib/rcrewai/llm_clients/azure.rb +23 -128
- data/lib/rcrewai/llm_clients/base.rb +11 -7
- data/lib/rcrewai/llm_clients/google.rb +159 -95
- data/lib/rcrewai/llm_clients/ollama.rb +150 -106
- data/lib/rcrewai/llm_clients/openai.rb +140 -63
- data/lib/rcrewai/mcp/client.rb +101 -0
- data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
- data/lib/rcrewai/mcp/transport/http.rb +53 -0
- data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
- data/lib/rcrewai/mcp.rb +8 -0
- data/lib/rcrewai/memory.rb +45 -37
- data/lib/rcrewai/pricing.rb +34 -0
- data/lib/rcrewai/process.rb +86 -95
- data/lib/rcrewai/provider_schema.rb +38 -0
- data/lib/rcrewai/sse_parser.rb +55 -0
- data/lib/rcrewai/task.rb +56 -64
- data/lib/rcrewai/tool_runner.rb +132 -0
- data/lib/rcrewai/tool_schema.rb +97 -0
- data/lib/rcrewai/tools/base.rb +98 -37
- data/lib/rcrewai/tools/code_executor.rb +71 -74
- data/lib/rcrewai/tools/email_sender.rb +70 -78
- data/lib/rcrewai/tools/file_reader.rb +38 -30
- data/lib/rcrewai/tools/file_writer.rb +40 -38
- data/lib/rcrewai/tools/pdf_processor.rb +115 -130
- data/lib/rcrewai/tools/sql_database.rb +58 -55
- data/lib/rcrewai/tools/web_search.rb +26 -25
- data/lib/rcrewai/version.rb +2 -2
- data/lib/rcrewai.rb +18 -10
- data/rcrewai.gemspec +39 -39
- metadata +65 -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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
87
|
+
@logger.info 'Shutting down async executor...'
|
|
90
88
|
@thread_pool.shutdown
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
|
12
|
-
option :crew, type: :string, required: true, desc:
|
|
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
|
|
20
|
+
desc 'list', 'List all available crews'
|
|
21
21
|
def list
|
|
22
|
-
puts
|
|
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
|
|
29
|
-
subcommand
|
|
28
|
+
desc 'agent SUBCOMMAND ...ARGS', 'Manage agents'
|
|
29
|
+
subcommand 'agent', Agent::CLI
|
|
30
30
|
|
|
31
|
-
desc
|
|
32
|
-
subcommand
|
|
31
|
+
desc 'task SUBCOMMAND ...ARGS', 'Manage tasks'
|
|
32
|
+
subcommand 'task', Task::CLI
|
|
33
33
|
|
|
34
|
-
desc
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
@@ -55,7 +60,7 @@ module RCrewAI
|
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
def validate!
|
|
58
|
-
raise ConfigurationError,
|
|
63
|
+
raise ConfigurationError, 'LLM provider must be set' if @llm_provider.nil?
|
|
59
64
|
raise ConfigurationError, "API key must be set for #{@llm_provider}" if api_key.nil? || api_key.empty?
|
|
60
65
|
raise ConfigurationError, "Model must be set for #{@llm_provider}" if model.nil? || model.empty?
|
|
61
66
|
end
|
|
@@ -75,7 +80,7 @@ module RCrewAI
|
|
|
75
80
|
@anthropic_api_key = ENV['ANTHROPIC_API_KEY'] || ENV['CLAUDE_API_KEY']
|
|
76
81
|
@google_api_key = ENV['GOOGLE_API_KEY'] || ENV['GEMINI_API_KEY']
|
|
77
82
|
@azure_api_key = ENV['AZURE_OPENAI_API_KEY']
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
@api_key = ENV['LLM_API_KEY'] if @api_key.nil?
|
|
80
85
|
@base_url = ENV['LLM_BASE_URL'] if @base_url.nil?
|
|
81
86
|
@api_version = ENV['AZURE_API_VERSION'] if @api_version.nil?
|
|
@@ -97,4 +102,4 @@ module RCrewAI
|
|
|
97
102
|
def self.reset_configuration!
|
|
98
103
|
@configuration = Configuration.new
|
|
99
104
|
end
|
|
100
|
-
end
|
|
105
|
+
end
|
data/lib/rcrewai/crew.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'process'
|
|
4
4
|
require_relative 'async_executor'
|
|
5
|
+
require_relative 'events'
|
|
5
6
|
|
|
6
7
|
module RCrewAI
|
|
7
8
|
class Crew
|
|
@@ -28,7 +29,12 @@ module RCrewAI
|
|
|
28
29
|
@tasks << task
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def execute(async: false, **async_options)
|
|
32
|
+
def execute(async: false, stream: nil, **async_options, &block)
|
|
33
|
+
sinks = []
|
|
34
|
+
sinks << block if block_given?
|
|
35
|
+
Array(stream).each { |s| sinks << s } if stream
|
|
36
|
+
@stream_sink = sinks.empty? ? nil : RCrewAI::Events.fan_out(sinks)
|
|
37
|
+
|
|
32
38
|
if async
|
|
33
39
|
execute_async(**async_options)
|
|
34
40
|
else
|
|
@@ -36,9 +42,11 @@ module RCrewAI
|
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
|
|
45
|
+
attr_reader :stream_sink
|
|
46
|
+
|
|
39
47
|
def execute_async(**options)
|
|
40
48
|
puts "Executing crew: #{name} (async #{process_type} process)"
|
|
41
|
-
|
|
49
|
+
|
|
42
50
|
case process_type
|
|
43
51
|
when :sequential
|
|
44
52
|
execute_sequential_async(**options)
|
|
@@ -53,13 +61,13 @@ module RCrewAI
|
|
|
53
61
|
|
|
54
62
|
def execute_sync
|
|
55
63
|
puts "Executing crew: #{name} (#{process_type} process)"
|
|
56
|
-
|
|
64
|
+
|
|
57
65
|
# Create appropriate process instance
|
|
58
66
|
@process_instance = create_process_instance
|
|
59
|
-
|
|
67
|
+
|
|
60
68
|
# Execute using the process
|
|
61
69
|
results = @process_instance.execute
|
|
62
|
-
|
|
70
|
+
|
|
63
71
|
# Return formatted results
|
|
64
72
|
format_execution_results(results)
|
|
65
73
|
end
|
|
@@ -67,7 +75,7 @@ module RCrewAI
|
|
|
67
75
|
def process=(new_process)
|
|
68
76
|
@process_type = new_process.to_sym
|
|
69
77
|
validate_process_type!
|
|
70
|
-
@process_instance = nil
|
|
78
|
+
@process_instance = nil # Reset process instance
|
|
71
79
|
end
|
|
72
80
|
|
|
73
81
|
def self.create(name)
|
|
@@ -84,7 +92,7 @@ module RCrewAI
|
|
|
84
92
|
|
|
85
93
|
def self.list
|
|
86
94
|
# List all available crews
|
|
87
|
-
[
|
|
95
|
+
%w[example_crew research_crew development_crew]
|
|
88
96
|
end
|
|
89
97
|
|
|
90
98
|
def save
|
|
@@ -95,10 +103,10 @@ module RCrewAI
|
|
|
95
103
|
private
|
|
96
104
|
|
|
97
105
|
def validate_process_type!
|
|
98
|
-
valid_processes = [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
valid_processes = %i[sequential hierarchical consensual]
|
|
107
|
+
return if valid_processes.include?(process_type)
|
|
108
|
+
|
|
109
|
+
raise ConfigurationError, "Invalid process type: #{process_type}. Valid types: #{valid_processes.join(', ')}"
|
|
102
110
|
end
|
|
103
111
|
|
|
104
112
|
def create_process_instance
|
|
@@ -116,11 +124,11 @@ module RCrewAI
|
|
|
116
124
|
|
|
117
125
|
def execute_sequential_async(**options)
|
|
118
126
|
executor = AsyncExecutor.new(**options)
|
|
119
|
-
|
|
127
|
+
|
|
120
128
|
begin
|
|
121
129
|
dependency_graph = build_dependency_graph
|
|
122
130
|
results = executor.execute_tasks_async(tasks, dependency_graph)
|
|
123
|
-
|
|
131
|
+
|
|
124
132
|
# Add crew metadata
|
|
125
133
|
results.merge(
|
|
126
134
|
crew: name,
|
|
@@ -134,38 +142,36 @@ module RCrewAI
|
|
|
134
142
|
def execute_hierarchical_async(**options)
|
|
135
143
|
# For hierarchical async, we need to coordinate through the manager
|
|
136
144
|
manager_agent = find_manager_agent
|
|
137
|
-
unless manager_agent
|
|
138
|
-
raise Process::ProcessError, "Hierarchical async execution requires a manager agent"
|
|
139
|
-
end
|
|
145
|
+
raise Process::ProcessError, 'Hierarchical async execution requires a manager agent' unless manager_agent
|
|
140
146
|
|
|
141
147
|
puts "Manager #{manager_agent.name} coordinating async execution"
|
|
142
|
-
|
|
148
|
+
|
|
143
149
|
executor = AsyncExecutor.new(**options)
|
|
144
|
-
|
|
150
|
+
|
|
145
151
|
begin
|
|
146
152
|
# Build execution phases respecting dependencies
|
|
147
153
|
dependency_graph = build_dependency_graph
|
|
148
154
|
phases = organize_tasks_into_phases(tasks, dependency_graph)
|
|
149
|
-
|
|
155
|
+
|
|
150
156
|
results = []
|
|
151
157
|
phases.each_with_index do |phase_tasks, phase_index|
|
|
152
158
|
puts "Manager delegating phase #{phase_index + 1}: #{phase_tasks.length} tasks"
|
|
153
|
-
|
|
159
|
+
|
|
154
160
|
# Create delegation contexts for each task
|
|
155
161
|
phase_tasks.each do |task|
|
|
156
162
|
unless task.agent
|
|
157
163
|
# Manager assigns best agent for the task
|
|
158
164
|
task.instance_variable_set(:@agent, find_best_agent_for_task(task))
|
|
159
165
|
end
|
|
160
|
-
|
|
166
|
+
|
|
161
167
|
# Add delegation context
|
|
162
168
|
add_delegation_context(task, manager_agent)
|
|
163
169
|
end
|
|
164
|
-
|
|
170
|
+
|
|
165
171
|
# Execute phase concurrently
|
|
166
172
|
phase_results = executor.execute_tasks_async(phase_tasks, {})
|
|
167
173
|
results.concat(phase_results[:results])
|
|
168
|
-
|
|
174
|
+
|
|
169
175
|
# Check if we should abort
|
|
170
176
|
failed_count = phase_results[:results].count { |r| r[:status] == :failed }
|
|
171
177
|
if failed_count > phase_tasks.length * 0.5
|
|
@@ -173,7 +179,7 @@ module RCrewAI
|
|
|
173
179
|
break
|
|
174
180
|
end
|
|
175
181
|
end
|
|
176
|
-
|
|
182
|
+
|
|
177
183
|
{
|
|
178
184
|
crew: name,
|
|
179
185
|
process: :async_hierarchical,
|
|
@@ -182,7 +188,13 @@ module RCrewAI
|
|
|
182
188
|
completed_tasks: results.count { |r| r[:status] == :completed },
|
|
183
189
|
failed_tasks: results.count { |r| r[:status] == :failed },
|
|
184
190
|
results: results,
|
|
185
|
-
success_rate: results.empty?
|
|
191
|
+
success_rate: if results.empty?
|
|
192
|
+
0
|
|
193
|
+
else
|
|
194
|
+
(results.count do |r|
|
|
195
|
+
r[:status] == :completed
|
|
196
|
+
end.to_f / results.length * 100).round(1)
|
|
197
|
+
end
|
|
186
198
|
}
|
|
187
199
|
ensure
|
|
188
200
|
executor.shutdown
|
|
@@ -192,11 +204,11 @@ module RCrewAI
|
|
|
192
204
|
def execute_consensual_async(**options)
|
|
193
205
|
# Simplified consensual async - execute in parallel with result aggregation
|
|
194
206
|
executor = AsyncExecutor.new(**options)
|
|
195
|
-
|
|
207
|
+
|
|
196
208
|
begin
|
|
197
209
|
# All tasks can potentially run in parallel for consensual
|
|
198
210
|
results = executor.execute_tasks_async(tasks, {})
|
|
199
|
-
|
|
211
|
+
|
|
200
212
|
results.merge(
|
|
201
213
|
crew: name,
|
|
202
214
|
process: :async_consensual
|
|
@@ -239,30 +251,29 @@ module RCrewAI
|
|
|
239
251
|
end
|
|
240
252
|
|
|
241
253
|
def find_manager_agent
|
|
242
|
-
agents.find
|
|
243
|
-
|
|
254
|
+
agents.find(&:is_manager?) ||
|
|
255
|
+
agents.find(&:allow_delegation)
|
|
244
256
|
end
|
|
245
257
|
|
|
246
258
|
def find_best_agent_for_task(task)
|
|
247
259
|
# Simple heuristic: match keywords
|
|
248
260
|
task_keywords = extract_keywords(task.description.downcase)
|
|
249
|
-
|
|
261
|
+
|
|
250
262
|
# Filter out manager agents first
|
|
251
263
|
non_manager_agents = agents.reject(&:is_manager?)
|
|
252
264
|
return nil if non_manager_agents.empty?
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
|
|
266
|
+
non_manager_agents.max_by do |agent|
|
|
255
267
|
agent_keywords = extract_keywords("#{agent.role} #{agent.goal}".downcase)
|
|
256
268
|
common_keywords = (task_keywords & agent_keywords).length
|
|
257
269
|
tool_bonus = agent.tools.any? ? 0.5 : 0
|
|
258
270
|
common_keywords + tool_bonus
|
|
259
271
|
end
|
|
260
|
-
|
|
261
|
-
best_agent
|
|
262
272
|
end
|
|
263
273
|
|
|
264
274
|
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
|
|
275
|
+
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
|
|
276
|
+
did will would could should]
|
|
266
277
|
text.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
|
|
267
278
|
end
|
|
268
279
|
|
|
@@ -270,10 +281,10 @@ module RCrewAI
|
|
|
270
281
|
delegation_context = {
|
|
271
282
|
manager: manager_agent.name,
|
|
272
283
|
delegation_reason: "Assigned by #{manager_agent.role} for optimal task execution",
|
|
273
|
-
coordination_notes:
|
|
284
|
+
coordination_notes: 'Part of async hierarchical execution',
|
|
274
285
|
async_execution: true
|
|
275
286
|
}
|
|
276
|
-
|
|
287
|
+
|
|
277
288
|
task.instance_variable_set(:@delegation_context, delegation_context)
|
|
278
289
|
end
|
|
279
290
|
|
|
@@ -285,8 +296,14 @@ module RCrewAI
|
|
|
285
296
|
completed_tasks: results.count { |r| r[:status] == :completed },
|
|
286
297
|
failed_tasks: results.count { |r| r[:status] == :failed },
|
|
287
298
|
results: results,
|
|
288
|
-
success_rate: results.empty?
|
|
299
|
+
success_rate: if results.empty?
|
|
300
|
+
0.0
|
|
301
|
+
else
|
|
302
|
+
(results.count do |r|
|
|
303
|
+
r[:status] == :completed
|
|
304
|
+
end.to_f / results.length * 100).round(1)
|
|
305
|
+
end
|
|
289
306
|
}
|
|
290
307
|
end
|
|
291
308
|
end
|
|
292
|
-
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
module Events
|
|
5
|
+
BASE_ATTRS = %i[type timestamp agent iteration].freeze
|
|
6
|
+
|
|
7
|
+
Event = Struct.new(*BASE_ATTRS, keyword_init: true)
|
|
8
|
+
TextDelta = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
|
|
9
|
+
TextDone = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
|
|
10
|
+
ToolCallStart = Struct.new(*BASE_ATTRS, :tool, :args, :call_id, keyword_init: true)
|
|
11
|
+
ToolCallResult = Struct.new(*BASE_ATTRS, :tool, :call_id, :result, :duration_ms, keyword_init: true)
|
|
12
|
+
ToolCallError = Struct.new(*BASE_ATTRS, :tool, :call_id, :error, keyword_init: true)
|
|
13
|
+
Thinking = Struct.new(*BASE_ATTRS, :text, keyword_init: true)
|
|
14
|
+
Usage = Struct.new(*BASE_ATTRS, :prompt_tokens, :completion_tokens, :total_tokens, :cost_usd, keyword_init: true)
|
|
15
|
+
IterationStart = Struct.new(*BASE_ATTRS, :iteration_index, keyword_init: true)
|
|
16
|
+
IterationEnd = Struct.new(*BASE_ATTRS, :finish_reason, keyword_init: true)
|
|
17
|
+
Error = Struct.new(*BASE_ATTRS, :error, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
def self.fan_out(sinks)
|
|
20
|
+
sinks = Array(sinks).compact
|
|
21
|
+
lambda do |event|
|
|
22
|
+
sinks.each do |s|
|
|
23
|
+
s.call(event)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Kernel.warn "[rcrewai] event sink raised: #{e.class}: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|