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
data/lib/rcrewai/task.rb
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'async_executor'
|
4
|
+
require_relative 'human_input'
|
5
|
+
|
6
|
+
module RCrewAI
|
7
|
+
class Task
|
8
|
+
include AsyncExtensions
|
9
|
+
include HumanInteractionExtensions
|
10
|
+
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async
|
11
|
+
attr_accessor :result, :status, :start_time, :end_time, :execution_time
|
12
|
+
|
13
|
+
def initialize(name:, description:, agent: nil, **options)
|
14
|
+
@name = name
|
15
|
+
@description = description
|
16
|
+
@agent = agent
|
17
|
+
@expected_output = options[:expected_output]
|
18
|
+
@context = options[:context] || [] # Tasks this task depends on
|
19
|
+
@tools = options[:tools] || [] # Additional tools for this specific task
|
20
|
+
@async = options[:async] || false # Whether task can run asynchronously
|
21
|
+
@callback = options[:callback] # Callback function after completion
|
22
|
+
|
23
|
+
# Human interaction options
|
24
|
+
@human_input_enabled = options[:human_input] || false
|
25
|
+
@require_human_confirmation = options[:require_confirmation] || false
|
26
|
+
@allow_human_guidance = options[:allow_guidance] || false
|
27
|
+
@human_review_points = options[:human_review_points] || []
|
28
|
+
|
29
|
+
@result = nil
|
30
|
+
@status = :pending
|
31
|
+
@start_time = nil
|
32
|
+
@end_time = nil
|
33
|
+
@execution_time = nil
|
34
|
+
@retry_count = 0
|
35
|
+
@max_retries = options.fetch(:max_retries, 2)
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute
|
39
|
+
@start_time = Time.now
|
40
|
+
@status = :running
|
41
|
+
|
42
|
+
begin
|
43
|
+
# Human confirmation before starting if required
|
44
|
+
if @require_human_confirmation
|
45
|
+
confirmation_result = confirm_task_execution
|
46
|
+
unless confirmation_result[:approved]
|
47
|
+
@status = :cancelled
|
48
|
+
@result = "Task execution cancelled by human: #{confirmation_result[:reason]}"
|
49
|
+
return @result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if agent
|
54
|
+
validate_dependencies!
|
55
|
+
provide_context_to_agent
|
56
|
+
|
57
|
+
# Enable human input for agent if task allows it
|
58
|
+
if @human_input_enabled && agent.respond_to?(:enable_human_input)
|
59
|
+
agent.enable_human_input(
|
60
|
+
require_approval_for_tools: false,
|
61
|
+
require_approval_for_final_answer: @allow_human_guidance
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
@result = agent.execute_task(self)
|
66
|
+
|
67
|
+
# Post-execution human review if configured
|
68
|
+
if @human_input_enabled && @human_review_points.include?(:completion)
|
69
|
+
review_result = request_task_completion_review
|
70
|
+
if review_result && !review_result[:approved]
|
71
|
+
@result = handle_completion_review_feedback(review_result)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@status = :completed
|
76
|
+
else
|
77
|
+
@result = "Task requires an agent"
|
78
|
+
@status = :failed
|
79
|
+
end
|
80
|
+
|
81
|
+
@end_time = Time.now
|
82
|
+
@execution_time = @end_time - @start_time
|
83
|
+
|
84
|
+
# Execute callback if provided
|
85
|
+
@callback&.call(self, @result)
|
86
|
+
|
87
|
+
@result
|
88
|
+
|
89
|
+
rescue TaskDependencyError => e
|
90
|
+
# Don't retry dependency errors - they need dependencies to be completed first
|
91
|
+
@status = :failed
|
92
|
+
@end_time = Time.now
|
93
|
+
@execution_time = @end_time - @start_time if @start_time
|
94
|
+
@result = "Task dependencies not met: #{e.message}"
|
95
|
+
raise e
|
96
|
+
rescue => e
|
97
|
+
@status = :failed
|
98
|
+
@end_time = Time.now
|
99
|
+
@execution_time = @end_time - @start_time if @start_time
|
100
|
+
|
101
|
+
# Retry logic with human intervention
|
102
|
+
if @retry_count < @max_retries
|
103
|
+
@retry_count += 1
|
104
|
+
puts "Task #{name} failed, retrying (#{@retry_count}/#{@max_retries})"
|
105
|
+
|
106
|
+
# Ask human for retry guidance if enabled
|
107
|
+
if @human_input_enabled
|
108
|
+
retry_decision = request_retry_guidance(e)
|
109
|
+
case retry_decision[:choice]
|
110
|
+
when 'abort'
|
111
|
+
@result = "Task execution aborted by human after failure: #{e.message}"
|
112
|
+
raise TaskExecutionError, "Task '#{name}' aborted by human"
|
113
|
+
when 'modify'
|
114
|
+
# Allow human to modify task parameters
|
115
|
+
modification_result = request_task_modification
|
116
|
+
if modification_result[:modified]
|
117
|
+
apply_task_modifications(modification_result[:changes])
|
118
|
+
end
|
119
|
+
# 'retry' is the default - continue with retry logic
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@status = :pending
|
124
|
+
sleep(2 ** @retry_count) # Exponential backoff
|
125
|
+
return execute
|
126
|
+
end
|
127
|
+
|
128
|
+
@result = "Task failed after #{@max_retries} retries: #{e.message}"
|
129
|
+
raise TaskExecutionError, "Task '#{name}' failed: #{e.message}"
|
130
|
+
ensure
|
131
|
+
# Disable human input for agent after task completion
|
132
|
+
if @human_input_enabled && agent.respond_to?(:disable_human_input)
|
133
|
+
agent.disable_human_input
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def context_data
|
139
|
+
return "" if context.empty?
|
140
|
+
|
141
|
+
context_results = context.map do |task|
|
142
|
+
if task.completed?
|
143
|
+
"Task: #{task.name}\nResult: #{task.result}\n---"
|
144
|
+
else
|
145
|
+
"Task: #{task.name}\nStatus: #{task.status}\n---"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
"Context from previous tasks:\n#{context_results.join("\n")}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def completed?
|
153
|
+
@status == :completed
|
154
|
+
end
|
155
|
+
|
156
|
+
def failed?
|
157
|
+
@status == :failed
|
158
|
+
end
|
159
|
+
|
160
|
+
def running?
|
161
|
+
@status == :running
|
162
|
+
end
|
163
|
+
|
164
|
+
def pending?
|
165
|
+
@status == :pending
|
166
|
+
end
|
167
|
+
|
168
|
+
def dependencies_met?
|
169
|
+
context.all?(&:completed?)
|
170
|
+
end
|
171
|
+
|
172
|
+
def add_context_task(task)
|
173
|
+
@context << task unless @context.include?(task)
|
174
|
+
end
|
175
|
+
|
176
|
+
def add_tool(tool)
|
177
|
+
@tools << tool unless @tools.include?(tool)
|
178
|
+
end
|
179
|
+
|
180
|
+
def enable_human_input(**options)
|
181
|
+
@human_input_enabled = true
|
182
|
+
@require_human_confirmation = options.fetch(:require_confirmation, false)
|
183
|
+
@allow_human_guidance = options.fetch(:allow_guidance, false)
|
184
|
+
@human_review_points = options.fetch(:review_points, [])
|
185
|
+
end
|
186
|
+
|
187
|
+
def disable_human_input
|
188
|
+
@human_input_enabled = false
|
189
|
+
@require_human_confirmation = false
|
190
|
+
@allow_human_guidance = false
|
191
|
+
@human_review_points = []
|
192
|
+
end
|
193
|
+
|
194
|
+
def human_input_enabled?
|
195
|
+
@human_input_enabled
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def confirm_task_execution
|
201
|
+
message = "Confirm execution of task: #{name}"
|
202
|
+
context = "Description: #{description}\nExpected Output: #{expected_output || 'Not specified'}\nAssigned Agent: #{agent&.name || 'No agent'}"
|
203
|
+
consequences = "The task will be executed with the specified agent and may use external tools."
|
204
|
+
|
205
|
+
request_human_approval(message,
|
206
|
+
context: context,
|
207
|
+
consequences: consequences,
|
208
|
+
timeout: 60
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
212
|
+
def request_task_completion_review
|
213
|
+
review_content = <<~CONTENT
|
214
|
+
Task: #{name}
|
215
|
+
Description: #{description}
|
216
|
+
Expected Output: #{expected_output || 'Not specified'}
|
217
|
+
|
218
|
+
Actual Result:
|
219
|
+
#{result}
|
220
|
+
|
221
|
+
Execution Time: #{execution_time&.round(2)} seconds
|
222
|
+
Agent: #{agent&.name || 'Unknown'}
|
223
|
+
CONTENT
|
224
|
+
|
225
|
+
request_human_review(
|
226
|
+
review_content,
|
227
|
+
review_criteria: ["Accuracy", "Completeness", "Meets expectations"],
|
228
|
+
timeout: 120
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def handle_completion_review_feedback(review_result)
|
233
|
+
if review_result[:suggested_changes] && review_result[:suggested_changes].any?
|
234
|
+
# Ask human how to handle the feedback
|
235
|
+
choice_result = request_human_choice(
|
236
|
+
"Task completed but received feedback. How should I proceed?",
|
237
|
+
[
|
238
|
+
"Accept result as-is despite feedback",
|
239
|
+
"Request agent revision based on feedback",
|
240
|
+
"Let me provide the correct result"
|
241
|
+
],
|
242
|
+
timeout: 60
|
243
|
+
)
|
244
|
+
|
245
|
+
case choice_result[:choice_index]
|
246
|
+
when 0
|
247
|
+
result # Return original result
|
248
|
+
when 1
|
249
|
+
# Request agent to revise (simplified)
|
250
|
+
revised_result = "#{result}\n\nNote: Human feedback received: #{review_result[:feedback]}"
|
251
|
+
revised_result
|
252
|
+
when 2
|
253
|
+
# Get corrected result from human
|
254
|
+
correction_result = request_human_input(
|
255
|
+
"Please provide the corrected task result:",
|
256
|
+
help_text: "Enter the complete, corrected result for this task"
|
257
|
+
)
|
258
|
+
correction_result[:input] || result
|
259
|
+
else
|
260
|
+
result
|
261
|
+
end
|
262
|
+
else
|
263
|
+
result
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def request_retry_guidance(error)
|
268
|
+
choices = [
|
269
|
+
"Retry with current settings",
|
270
|
+
"Modify task parameters and retry",
|
271
|
+
"Abort task execution"
|
272
|
+
]
|
273
|
+
|
274
|
+
choice_result = request_human_choice(
|
275
|
+
"Task '#{name}' failed with error: #{error.message}. How should I proceed?",
|
276
|
+
choices,
|
277
|
+
timeout: 120
|
278
|
+
)
|
279
|
+
|
280
|
+
{
|
281
|
+
choice: case choice_result[:choice_index]
|
282
|
+
when 0 then 'retry'
|
283
|
+
when 1 then 'modify'
|
284
|
+
when 2 then 'abort'
|
285
|
+
else 'retry'
|
286
|
+
end,
|
287
|
+
reason: choice_result[:choice] || 'Default retry'
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
def request_task_modification
|
292
|
+
modification_input = request_human_input(
|
293
|
+
"Please specify task modifications (JSON format):",
|
294
|
+
type: :json,
|
295
|
+
help_text: "Provide modifications as JSON, e.g. {\"description\": \"new description\", \"expected_output\": \"new output\"}"
|
296
|
+
)
|
297
|
+
|
298
|
+
if modification_input[:valid]
|
299
|
+
{
|
300
|
+
modified: true,
|
301
|
+
changes: modification_input[:processed_input]
|
302
|
+
}
|
303
|
+
else
|
304
|
+
{
|
305
|
+
modified: false,
|
306
|
+
reason: modification_input[:reason]
|
307
|
+
}
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def apply_task_modifications(changes)
|
312
|
+
changes.each do |key, value|
|
313
|
+
case key.to_s
|
314
|
+
when 'description'
|
315
|
+
@description = value
|
316
|
+
when 'expected_output'
|
317
|
+
@expected_output = value
|
318
|
+
when 'max_retries'
|
319
|
+
@max_retries = value.to_i if value.to_i > 0
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def validate_dependencies!
|
325
|
+
unless dependencies_met?
|
326
|
+
incomplete_deps = context.reject(&:completed?).map(&:name)
|
327
|
+
raise TaskDependencyError, "Dependencies not met: #{incomplete_deps.join(', ')}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def provide_context_to_agent
|
332
|
+
# Temporarily add task-specific tools to agent
|
333
|
+
original_tools = agent.tools.dup
|
334
|
+
combined_tools = (agent.tools + @tools).uniq
|
335
|
+
|
336
|
+
# Use metaprogramming to temporarily extend agent's tools
|
337
|
+
agent.instance_variable_set(:@tools, combined_tools)
|
338
|
+
|
339
|
+
# Restore original tools after execution (handled by ensure in execute method)
|
340
|
+
at_exit { agent.instance_variable_set(:@tools, original_tools) }
|
341
|
+
end
|
342
|
+
|
343
|
+
class CLI < Thor
|
344
|
+
desc "new NAME", "Create a new task"
|
345
|
+
option :description, type: :string, required: true
|
346
|
+
option :agent, type: :string
|
347
|
+
option :expected_output, type: :string
|
348
|
+
option :async, type: :boolean, default: false
|
349
|
+
def new(name)
|
350
|
+
task = Task.new(
|
351
|
+
name: name,
|
352
|
+
description: options[:description],
|
353
|
+
agent: options[:agent],
|
354
|
+
expected_output: options[:expected_output],
|
355
|
+
async: options[:async]
|
356
|
+
)
|
357
|
+
puts "Task '#{name}' created"
|
358
|
+
puts "Description: #{options[:description]}"
|
359
|
+
puts "Expected Output: #{options[:expected_output] || 'Not specified'}"
|
360
|
+
puts "Assigned to: #{options[:agent] || 'No agent assigned'}"
|
361
|
+
puts "Async: #{options[:async]}"
|
362
|
+
end
|
363
|
+
|
364
|
+
desc "list", "List all tasks"
|
365
|
+
def list
|
366
|
+
puts "Available tasks:"
|
367
|
+
puts " - research_topic (Agent: researcher)"
|
368
|
+
puts " - write_article (Agent: writer)"
|
369
|
+
puts " - analyze_data (Agent: analyst)"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
class TaskExecutionError < Error; end
|
375
|
+
class TaskDependencyError < TaskExecutionError; end
|
376
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RCrewAI
|
4
|
+
module Tools
|
5
|
+
class Base
|
6
|
+
attr_reader :name, :description
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@name = self.class.name.split('::').last.downcase
|
10
|
+
@description = "Base tool class"
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(**params)
|
14
|
+
raise NotImplementedError, "Subclasses must implement #execute method"
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate_params!(params, required: [], optional: [])
|
18
|
+
# Check required parameters
|
19
|
+
missing = required - params.keys
|
20
|
+
unless missing.empty?
|
21
|
+
raise ToolError, "Missing required parameters: #{missing.join(', ')}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check for unexpected parameters
|
25
|
+
allowed = required + optional
|
26
|
+
unexpected = params.keys - allowed
|
27
|
+
unless unexpected.empty?
|
28
|
+
raise ToolError, "Unexpected parameters: #{unexpected.join(', ')}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.available_tools
|
33
|
+
[
|
34
|
+
WebSearch,
|
35
|
+
FileReader,
|
36
|
+
FileWriter,
|
37
|
+
SqlDatabase,
|
38
|
+
EmailSender,
|
39
|
+
CodeExecutor,
|
40
|
+
PdfProcessor
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.create_tool(tool_name, **options)
|
45
|
+
tool_class = case tool_name.to_s.downcase
|
46
|
+
when 'websearch', 'web_search'
|
47
|
+
WebSearch
|
48
|
+
when 'filereader', 'file_reader'
|
49
|
+
FileReader
|
50
|
+
when 'filewriter', 'file_writer'
|
51
|
+
FileWriter
|
52
|
+
when 'sqldatabase', 'sql_database', 'database'
|
53
|
+
SqlDatabase
|
54
|
+
when 'emailsender', 'email_sender', 'email'
|
55
|
+
EmailSender
|
56
|
+
when 'codeexecutor', 'code_executor', 'code'
|
57
|
+
CodeExecutor
|
58
|
+
when 'pdfprocessor', 'pdf_processor', 'pdf'
|
59
|
+
PdfProcessor
|
60
|
+
else
|
61
|
+
raise ToolError, "Unknown tool: #{tool_name}"
|
62
|
+
end
|
63
|
+
|
64
|
+
tool_class.new(**options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.list_available_tools
|
68
|
+
{
|
69
|
+
'websearch' => 'Search the web using DuckDuckGo',
|
70
|
+
'filereader' => 'Read contents from text files',
|
71
|
+
'filewriter' => 'Write content to text files',
|
72
|
+
'sqldatabase' => 'Execute SQL queries against databases',
|
73
|
+
'emailsender' => 'Send emails via SMTP',
|
74
|
+
'codeexecutor' => 'Execute code in various programming languages',
|
75
|
+
'pdfprocessor' => 'Read and extract text from PDF files'
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class ToolError < RCrewAI::Error; end
|
81
|
+
end
|
82
|
+
end
|