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.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +99 -0
- data/CHANGELOG.md +64 -1
- data/README.md +170 -2
- data/ROADMAP.md +84 -0
- 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 +181 -286
- data/lib/rcrewai/async_executor.rb +43 -43
- data/lib/rcrewai/cli.rb +11 -11
- data/lib/rcrewai/configuration.rb +34 -9
- data/lib/rcrewai/crew.rb +134 -39
- data/lib/rcrewai/events.rb +30 -0
- data/lib/rcrewai/flow/state.rb +47 -0
- data/lib/rcrewai/flow/state_store.rb +50 -0
- data/lib/rcrewai/flow.rb +243 -0
- data/lib/rcrewai/human_input.rb +104 -114
- data/lib/rcrewai/knowledge/base.rb +52 -0
- data/lib/rcrewai/knowledge/chunker.rb +31 -0
- data/lib/rcrewai/knowledge/embedder.rb +48 -0
- data/lib/rcrewai/knowledge/sources.rb +83 -0
- data/lib/rcrewai/knowledge/store.rb +58 -0
- data/lib/rcrewai/knowledge.rb +13 -0
- data/lib/rcrewai/legacy_react_runner.rb +172 -0
- data/lib/rcrewai/llm_client.rb +24 -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/output_schema.rb +79 -0
- data/lib/rcrewai/planning.rb +65 -0
- 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 +145 -66
- 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 +20 -10
- data/rcrewai.gemspec +39 -39
- 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
|
|
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
|
|
@@ -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,
|
|
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
|
|
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
|
-
[
|
|
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 = [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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?
|
|
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
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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?
|
|
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
|