rcrewai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +108 -0
  3. data/LICENSE +21 -0
  4. data/README.md +328 -0
  5. data/Rakefile +130 -0
  6. data/bin/rcrewai +7 -0
  7. data/docs/_config.yml +59 -0
  8. data/docs/_layouts/api.html +16 -0
  9. data/docs/_layouts/default.html +78 -0
  10. data/docs/_layouts/example.html +24 -0
  11. data/docs/_layouts/tutorial.html +33 -0
  12. data/docs/api/configuration.md +327 -0
  13. data/docs/api/crew.md +345 -0
  14. data/docs/api/index.md +41 -0
  15. data/docs/api/tools.md +412 -0
  16. data/docs/assets/css/style.css +416 -0
  17. data/docs/examples/human-in-the-loop.md +382 -0
  18. data/docs/examples/index.md +78 -0
  19. data/docs/examples/production-ready-crew.md +485 -0
  20. data/docs/examples/simple-research-crew.md +297 -0
  21. data/docs/index.md +353 -0
  22. data/docs/tutorials/getting-started.md +341 -0
  23. data/examples/async_execution_example.rb +294 -0
  24. data/examples/hierarchical_crew_example.rb +193 -0
  25. data/examples/human_in_the_loop_example.rb +233 -0
  26. data/lib/rcrewai/agent.rb +636 -0
  27. data/lib/rcrewai/async_executor.rb +248 -0
  28. data/lib/rcrewai/cli.rb +39 -0
  29. data/lib/rcrewai/configuration.rb +100 -0
  30. data/lib/rcrewai/crew.rb +292 -0
  31. data/lib/rcrewai/human_input.rb +520 -0
  32. data/lib/rcrewai/llm_client.rb +41 -0
  33. data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
  34. data/lib/rcrewai/llm_clients/azure.rb +158 -0
  35. data/lib/rcrewai/llm_clients/base.rb +82 -0
  36. data/lib/rcrewai/llm_clients/google.rb +158 -0
  37. data/lib/rcrewai/llm_clients/ollama.rb +199 -0
  38. data/lib/rcrewai/llm_clients/openai.rb +124 -0
  39. data/lib/rcrewai/memory.rb +194 -0
  40. data/lib/rcrewai/process.rb +421 -0
  41. data/lib/rcrewai/task.rb +376 -0
  42. data/lib/rcrewai/tools/base.rb +82 -0
  43. data/lib/rcrewai/tools/code_executor.rb +333 -0
  44. data/lib/rcrewai/tools/email_sender.rb +210 -0
  45. data/lib/rcrewai/tools/file_reader.rb +111 -0
  46. data/lib/rcrewai/tools/file_writer.rb +115 -0
  47. data/lib/rcrewai/tools/pdf_processor.rb +342 -0
  48. data/lib/rcrewai/tools/sql_database.rb +226 -0
  49. data/lib/rcrewai/tools/web_search.rb +131 -0
  50. data/lib/rcrewai/version.rb +5 -0
  51. data/lib/rcrewai.rb +36 -0
  52. data/rcrewai.gemspec +54 -0
  53. metadata +365 -0
@@ -0,0 +1,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