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
data/lib/rcrewai/human_input.rb
CHANGED
|
@@ -22,77 +22,77 @@ module RCrewAI
|
|
|
22
22
|
|
|
23
23
|
def request_approval(message, **options)
|
|
24
24
|
interaction = create_interaction(:approval, message, options)
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
return handle_auto_approval(interaction) if @auto_approve
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
display_approval_request(interaction)
|
|
29
29
|
response = get_user_input(interaction)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
result = process_approval_response(response, interaction)
|
|
32
32
|
record_interaction(interaction, response, result)
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
result
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def request_input(prompt, **options)
|
|
38
38
|
interaction = create_interaction(:input, prompt, options)
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
display_input_request(interaction)
|
|
41
41
|
response = get_user_input(interaction)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
result = process_input_response(response, interaction)
|
|
44
44
|
record_interaction(interaction, response, result)
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
result
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def request_choice(prompt, choices, **options)
|
|
50
50
|
interaction = create_interaction(:choice, prompt, options.merge(choices: choices))
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
display_choice_request(interaction)
|
|
53
53
|
response = get_user_input(interaction)
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
result = process_choice_response(response, interaction)
|
|
56
56
|
record_interaction(interaction, response, result)
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
result
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def request_review(content, **options)
|
|
62
62
|
interaction = create_interaction(:review, content, options)
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
display_review_request(interaction)
|
|
65
65
|
response = get_user_input(interaction)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
result = process_review_response(response, interaction)
|
|
68
68
|
record_interaction(interaction, response, result)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
result
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def confirm_action(action_description, **options)
|
|
74
74
|
interaction = create_interaction(:confirmation, action_description, options)
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
return handle_auto_approval(interaction) if @auto_approve
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
display_confirmation_request(interaction)
|
|
79
79
|
response = get_user_input(interaction)
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
result = process_confirmation_response(response, interaction)
|
|
82
82
|
record_interaction(interaction, response, result)
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
result
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def get_feedback(prompt, **options)
|
|
88
88
|
interaction = create_interaction(:feedback, prompt, options)
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
display_feedback_request(interaction)
|
|
91
91
|
response = get_user_input(interaction)
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
result = { feedback: response, timestamp: Time.now }
|
|
94
94
|
record_interaction(interaction, response, result)
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
result
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -131,122 +131,110 @@ module RCrewAI
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def display_approval_request(interaction)
|
|
134
|
-
puts "\n
|
|
135
|
-
puts
|
|
136
|
-
puts
|
|
134
|
+
puts "\n#{'=' * 60}"
|
|
135
|
+
puts '🤝 HUMAN APPROVAL REQUIRED'
|
|
136
|
+
puts '=' * 60
|
|
137
137
|
puts "Request: #{interaction[:content]}"
|
|
138
|
-
|
|
139
|
-
if interaction[:options][:context]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if interaction[:options][:consequences]
|
|
144
|
-
puts "\nConsequences: #{interaction[:options][:consequences]}"
|
|
145
|
-
end
|
|
146
|
-
|
|
138
|
+
|
|
139
|
+
puts "\nContext: #{interaction[:options][:context]}" if interaction[:options][:context]
|
|
140
|
+
|
|
141
|
+
puts "\nConsequences: #{interaction[:options][:consequences]}" if interaction[:options][:consequences]
|
|
142
|
+
|
|
147
143
|
puts "\nDo you approve this action? (yes/no)"
|
|
148
|
-
print
|
|
144
|
+
print '> '
|
|
149
145
|
end
|
|
150
146
|
|
|
151
147
|
def display_input_request(interaction)
|
|
152
|
-
puts "\n
|
|
153
|
-
puts
|
|
154
|
-
puts
|
|
148
|
+
puts "\n#{'=' * 60}"
|
|
149
|
+
puts '💬 HUMAN INPUT REQUESTED'
|
|
150
|
+
puts '=' * 60
|
|
155
151
|
puts "Prompt: #{interaction[:content]}"
|
|
156
|
-
|
|
157
|
-
if interaction[:options][:help_text]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if interaction[:options][:examples]
|
|
162
|
-
puts "\nExamples: #{interaction[:options][:examples].join(', ')}"
|
|
163
|
-
end
|
|
164
|
-
|
|
152
|
+
|
|
153
|
+
puts "\nHelp: #{interaction[:options][:help_text]}" if interaction[:options][:help_text]
|
|
154
|
+
|
|
155
|
+
puts "\nExamples: #{interaction[:options][:examples].join(', ')}" if interaction[:options][:examples]
|
|
156
|
+
|
|
165
157
|
puts "\nPlease provide your input:"
|
|
166
|
-
print
|
|
158
|
+
print '> '
|
|
167
159
|
end
|
|
168
160
|
|
|
169
161
|
def display_choice_request(interaction)
|
|
170
|
-
puts "\n
|
|
171
|
-
puts
|
|
172
|
-
puts
|
|
162
|
+
puts "\n#{'=' * 60}"
|
|
163
|
+
puts '🎯 HUMAN CHOICE REQUIRED'
|
|
164
|
+
puts '=' * 60
|
|
173
165
|
puts "Question: #{interaction[:content]}"
|
|
174
166
|
puts "\nAvailable choices:"
|
|
175
|
-
|
|
167
|
+
|
|
176
168
|
interaction[:options][:choices].each_with_index do |choice, index|
|
|
177
169
|
puts " #{index + 1}. #{choice}"
|
|
178
170
|
end
|
|
179
|
-
|
|
171
|
+
|
|
180
172
|
puts "\nPlease select a choice (enter number or text):"
|
|
181
|
-
print
|
|
173
|
+
print '> '
|
|
182
174
|
end
|
|
183
175
|
|
|
184
176
|
def display_review_request(interaction)
|
|
185
|
-
puts "\n
|
|
186
|
-
puts
|
|
187
|
-
puts
|
|
188
|
-
puts
|
|
189
|
-
puts
|
|
177
|
+
puts "\n#{'=' * 60}"
|
|
178
|
+
puts '👀 HUMAN REVIEW REQUESTED'
|
|
179
|
+
puts '=' * 60
|
|
180
|
+
puts 'Content to review:'
|
|
181
|
+
puts '-' * 40
|
|
190
182
|
puts interaction[:content]
|
|
191
|
-
puts
|
|
192
|
-
|
|
183
|
+
puts '-' * 40
|
|
184
|
+
|
|
193
185
|
if interaction[:options][:review_criteria]
|
|
194
186
|
puts "\nReview criteria: #{interaction[:options][:review_criteria].join(', ')}"
|
|
195
187
|
end
|
|
196
|
-
|
|
188
|
+
|
|
197
189
|
puts "\nPlease review and provide feedback (or type 'approve' to approve as-is):"
|
|
198
|
-
print
|
|
190
|
+
print '> '
|
|
199
191
|
end
|
|
200
192
|
|
|
201
193
|
def display_confirmation_request(interaction)
|
|
202
|
-
puts "\n
|
|
203
|
-
puts
|
|
204
|
-
puts
|
|
194
|
+
puts "\n#{'=' * 60}"
|
|
195
|
+
puts '⚠️ CONFIRMATION REQUIRED'
|
|
196
|
+
puts '=' * 60
|
|
205
197
|
puts "Action: #{interaction[:content]}"
|
|
206
|
-
|
|
207
|
-
if interaction[:options][:risk_level]
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if interaction[:options][:details]
|
|
212
|
-
puts "Details: #{interaction[:options][:details]}"
|
|
213
|
-
end
|
|
214
|
-
|
|
198
|
+
|
|
199
|
+
puts "Risk Level: #{interaction[:options][:risk_level]}" if interaction[:options][:risk_level]
|
|
200
|
+
|
|
201
|
+
puts "Details: #{interaction[:options][:details]}" if interaction[:options][:details]
|
|
202
|
+
|
|
215
203
|
puts "\nConfirm this action? (yes/no)"
|
|
216
|
-
print
|
|
204
|
+
print '> '
|
|
217
205
|
end
|
|
218
206
|
|
|
219
207
|
def display_feedback_request(interaction)
|
|
220
|
-
puts "\n
|
|
221
|
-
puts
|
|
222
|
-
puts
|
|
208
|
+
puts "\n#{'=' * 60}"
|
|
209
|
+
puts '📝 FEEDBACK REQUESTED'
|
|
210
|
+
puts '=' * 60
|
|
223
211
|
puts "Request: #{interaction[:content]}"
|
|
224
|
-
|
|
212
|
+
|
|
225
213
|
puts "\nPlease provide your feedback:"
|
|
226
|
-
print
|
|
214
|
+
print '> '
|
|
227
215
|
end
|
|
228
216
|
|
|
229
217
|
def get_user_input(interaction)
|
|
230
218
|
start_time = Time.now
|
|
231
|
-
|
|
219
|
+
|
|
232
220
|
begin
|
|
233
|
-
if
|
|
221
|
+
if $stdin.tty?
|
|
234
222
|
# Interactive terminal input with timeout
|
|
235
223
|
response = nil
|
|
236
224
|
input_thread = Thread.new do
|
|
237
|
-
response =
|
|
225
|
+
response = $stdin.gets&.chomp
|
|
238
226
|
end
|
|
239
|
-
|
|
227
|
+
|
|
240
228
|
unless input_thread.join(interaction[:timeout])
|
|
241
229
|
input_thread.kill
|
|
242
230
|
puts "\n⏰ Input timed out after #{interaction[:timeout]} seconds"
|
|
243
231
|
return nil
|
|
244
232
|
end
|
|
245
|
-
|
|
233
|
+
|
|
246
234
|
response
|
|
247
235
|
else
|
|
248
236
|
# Non-interactive mode (e.g., scripts, tests)
|
|
249
|
-
puts
|
|
237
|
+
puts '⚠️ Non-interactive mode detected, using default response'
|
|
250
238
|
get_default_response(interaction[:type])
|
|
251
239
|
end
|
|
252
240
|
rescue Interrupt
|
|
@@ -270,20 +258,20 @@ module RCrewAI
|
|
|
270
258
|
end
|
|
271
259
|
end
|
|
272
260
|
|
|
273
|
-
def process_approval_response(response,
|
|
261
|
+
def process_approval_response(response, _interaction)
|
|
274
262
|
return { approved: false, reason: 'No response provided' } if response.nil?
|
|
275
|
-
|
|
263
|
+
|
|
276
264
|
response_clean = response.strip.downcase
|
|
277
|
-
|
|
265
|
+
|
|
278
266
|
approved = if @approval_keywords.any? { |keyword| response_clean.include?(keyword) }
|
|
279
267
|
true
|
|
280
268
|
elsif @rejection_keywords.any? { |keyword| response_clean.include?(keyword) }
|
|
281
269
|
false
|
|
282
270
|
else
|
|
283
271
|
# Try to interpret ambiguous responses
|
|
284
|
-
response_clean.length
|
|
272
|
+
response_clean.length.positive? && !response_clean.start_with?('n')
|
|
285
273
|
end
|
|
286
|
-
|
|
274
|
+
|
|
287
275
|
{
|
|
288
276
|
approved: approved,
|
|
289
277
|
response: response,
|
|
@@ -294,10 +282,10 @@ module RCrewAI
|
|
|
294
282
|
|
|
295
283
|
def process_input_response(response, interaction)
|
|
296
284
|
return { input: nil, valid: false, reason: 'No response provided' } if response.nil?
|
|
297
|
-
|
|
285
|
+
|
|
298
286
|
# Validate input if validation rules provided
|
|
299
287
|
validation_result = validate_input(response, interaction[:options][:validation] || {})
|
|
300
|
-
|
|
288
|
+
|
|
301
289
|
{
|
|
302
290
|
input: response,
|
|
303
291
|
valid: validation_result[:valid],
|
|
@@ -309,10 +297,10 @@ module RCrewAI
|
|
|
309
297
|
|
|
310
298
|
def process_choice_response(response, interaction)
|
|
311
299
|
return { choice: nil, valid: false, reason: 'No response provided' } if response.nil?
|
|
312
|
-
|
|
300
|
+
|
|
313
301
|
choices = interaction[:options][:choices]
|
|
314
302
|
response_clean = response.strip
|
|
315
|
-
|
|
303
|
+
|
|
316
304
|
# Try to match by number
|
|
317
305
|
if response_clean.match?(/^\d+$/)
|
|
318
306
|
choice_index = response_clean.to_i - 1
|
|
@@ -327,7 +315,7 @@ module RCrewAI
|
|
|
327
315
|
}
|
|
328
316
|
end
|
|
329
317
|
end
|
|
330
|
-
|
|
318
|
+
|
|
331
319
|
# Try to match by text
|
|
332
320
|
selected_choice = choices.find { |choice| choice.downcase.include?(response_clean.downcase) }
|
|
333
321
|
if selected_choice
|
|
@@ -339,7 +327,7 @@ module RCrewAI
|
|
|
339
327
|
timestamp: Time.now
|
|
340
328
|
}
|
|
341
329
|
end
|
|
342
|
-
|
|
330
|
+
|
|
343
331
|
# Invalid selection
|
|
344
332
|
{
|
|
345
333
|
choice: nil,
|
|
@@ -350,11 +338,11 @@ module RCrewAI
|
|
|
350
338
|
}
|
|
351
339
|
end
|
|
352
340
|
|
|
353
|
-
def process_review_response(response,
|
|
341
|
+
def process_review_response(response, _interaction)
|
|
354
342
|
return { feedback: nil, approved: false, reason: 'No response provided' } if response.nil?
|
|
355
|
-
|
|
343
|
+
|
|
356
344
|
response_clean = response.strip.downcase
|
|
357
|
-
|
|
345
|
+
|
|
358
346
|
# Check if it's a simple approval
|
|
359
347
|
if @approval_keywords.any? { |keyword| response_clean == keyword }
|
|
360
348
|
return {
|
|
@@ -364,7 +352,7 @@ module RCrewAI
|
|
|
364
352
|
timestamp: Time.now
|
|
365
353
|
}
|
|
366
354
|
end
|
|
367
|
-
|
|
355
|
+
|
|
368
356
|
# Otherwise treat as feedback
|
|
369
357
|
{
|
|
370
358
|
feedback: response,
|
|
@@ -381,36 +369,38 @@ module RCrewAI
|
|
|
381
369
|
|
|
382
370
|
def validate_input(input, validation_rules)
|
|
383
371
|
return { valid: true, reason: 'No validation rules' } if validation_rules.empty?
|
|
384
|
-
|
|
372
|
+
|
|
385
373
|
# Check minimum length
|
|
386
374
|
if validation_rules[:min_length] && input.length < validation_rules[:min_length]
|
|
387
375
|
return { valid: false, reason: "Input too short (minimum #{validation_rules[:min_length]} characters)" }
|
|
388
376
|
end
|
|
389
|
-
|
|
377
|
+
|
|
390
378
|
# Check maximum length
|
|
391
379
|
if validation_rules[:max_length] && input.length > validation_rules[:max_length]
|
|
392
380
|
return { valid: false, reason: "Input too long (maximum #{validation_rules[:max_length]} characters)" }
|
|
393
381
|
end
|
|
394
|
-
|
|
382
|
+
|
|
395
383
|
# Check pattern matching
|
|
396
384
|
if validation_rules[:pattern] && !input.match?(validation_rules[:pattern])
|
|
397
385
|
return { valid: false, reason: "Input doesn't match required pattern" }
|
|
398
386
|
end
|
|
399
|
-
|
|
387
|
+
|
|
400
388
|
# Check required keywords
|
|
401
389
|
if validation_rules[:required_keywords]
|
|
402
|
-
missing_keywords = validation_rules[:required_keywords].reject
|
|
390
|
+
missing_keywords = validation_rules[:required_keywords].reject do |keyword|
|
|
391
|
+
input.downcase.include?(keyword.downcase)
|
|
392
|
+
end
|
|
403
393
|
unless missing_keywords.empty?
|
|
404
394
|
return { valid: false, reason: "Missing required keywords: #{missing_keywords.join(', ')}" }
|
|
405
395
|
end
|
|
406
396
|
end
|
|
407
|
-
|
|
397
|
+
|
|
408
398
|
{ valid: true, reason: 'Input passes validation' }
|
|
409
399
|
end
|
|
410
400
|
|
|
411
401
|
def process_input_value(input, options)
|
|
412
402
|
return input unless options[:type]
|
|
413
|
-
|
|
403
|
+
|
|
414
404
|
case options[:type]
|
|
415
405
|
when :integer
|
|
416
406
|
input.to_i
|
|
@@ -431,19 +421,19 @@ module RCrewAI
|
|
|
431
421
|
|
|
432
422
|
def extract_suggestions(feedback)
|
|
433
423
|
suggestions = []
|
|
434
|
-
|
|
424
|
+
|
|
435
425
|
# Simple pattern matching for common suggestion indicators
|
|
436
426
|
suggestion_patterns = [
|
|
437
427
|
/(?:should|could|might want to|consider|suggest|recommend)\s+(.+?)(?:\.|$)/i,
|
|
438
428
|
/(?:change|modify|update|fix|improve)\s+(.+?)(?:\.|$)/i,
|
|
439
429
|
/(?:add|include|remove|delete)\s+(.+?)(?:\.|$)/i
|
|
440
430
|
]
|
|
441
|
-
|
|
431
|
+
|
|
442
432
|
suggestion_patterns.each do |pattern|
|
|
443
433
|
matches = feedback.scan(pattern)
|
|
444
434
|
suggestions.concat(matches.flatten)
|
|
445
435
|
end
|
|
446
|
-
|
|
436
|
+
|
|
447
437
|
suggestions.map(&:strip).reject(&:empty?).uniq
|
|
448
438
|
end
|
|
449
439
|
|
|
@@ -462,20 +452,20 @@ module RCrewAI
|
|
|
462
452
|
interaction[:result] = result
|
|
463
453
|
interaction[:completed_at] = Time.now
|
|
464
454
|
interaction[:duration] = interaction[:completed_at] - interaction[:timestamp]
|
|
465
|
-
|
|
455
|
+
|
|
466
456
|
@interactions << interaction
|
|
467
457
|
@input_history << {
|
|
468
458
|
type: interaction[:type],
|
|
469
459
|
response: response,
|
|
470
460
|
timestamp: Time.now
|
|
471
461
|
}
|
|
472
|
-
|
|
462
|
+
|
|
473
463
|
@logger.info "Human interaction completed: #{interaction[:type]} - #{result[:reason] || 'Success'}"
|
|
474
464
|
end
|
|
475
465
|
|
|
476
466
|
def calculate_session_duration
|
|
477
467
|
return 0 if @interactions.empty?
|
|
478
|
-
|
|
468
|
+
|
|
479
469
|
first = @interactions.first[:timestamp]
|
|
480
470
|
last = @interactions.last[:completed_at] || @interactions.last[:timestamp]
|
|
481
471
|
last - first
|
|
@@ -517,4 +507,4 @@ module RCrewAI
|
|
|
517
507
|
)
|
|
518
508
|
end
|
|
519
509
|
end
|
|
520
|
-
end
|
|
510
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'events'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
# Behavior-preserving extraction of the prompt-parsed `USE_TOOL[]` /
|
|
7
|
+
# `FINAL_ANSWER[]` loop that lived in Agent. Used as a fallback when an
|
|
8
|
+
# agent's tools have no DSL schemas declared OR the configured LLM does
|
|
9
|
+
# not support native function calling.
|
|
10
|
+
class LegacyReactRunner
|
|
11
|
+
DEFAULT_MAX_ITERATIONS = 10
|
|
12
|
+
|
|
13
|
+
def initialize(agent:, llm:, tools:, max_iterations: DEFAULT_MAX_ITERATIONS, event_sink: nil)
|
|
14
|
+
@agent = agent
|
|
15
|
+
@llm = llm
|
|
16
|
+
@tools = tools
|
|
17
|
+
@max_iterations = max_iterations
|
|
18
|
+
@sink = event_sink || ->(_) {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(messages:)
|
|
22
|
+
msgs = messages.dup
|
|
23
|
+
history = []
|
|
24
|
+
iter = 0
|
|
25
|
+
total_usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
26
|
+
last_reasoning = nil
|
|
27
|
+
last_action_result = nil
|
|
28
|
+
|
|
29
|
+
while iter < @max_iterations
|
|
30
|
+
iter += 1
|
|
31
|
+
emit(Events::IterationStart, iteration: iter, iteration_index: iter)
|
|
32
|
+
|
|
33
|
+
response = @llm.chat(messages: msgs)
|
|
34
|
+
accumulate_usage(total_usage, response[:usage])
|
|
35
|
+
reasoning = response[:content] || ''
|
|
36
|
+
last_reasoning = reasoning
|
|
37
|
+
|
|
38
|
+
action_result, iteration_history = parse_and_execute_actions(reasoning, iter)
|
|
39
|
+
history.concat(iteration_history)
|
|
40
|
+
last_action_result = action_result
|
|
41
|
+
|
|
42
|
+
msgs << { role: 'assistant', content: reasoning }
|
|
43
|
+
msgs << { role: 'user', content: action_result } if action_result && !action_result.empty?
|
|
44
|
+
|
|
45
|
+
finish_reason = response[:finish_reason]
|
|
46
|
+
emit(Events::IterationEnd, iteration: iter, finish_reason: finish_reason)
|
|
47
|
+
|
|
48
|
+
next unless task_complete?(reasoning, action_result) || finish_reason == :stop
|
|
49
|
+
|
|
50
|
+
final = extract_final_result(reasoning, action_result)
|
|
51
|
+
return finalize(content: final, history: history, iter: iter,
|
|
52
|
+
finish_reason: finish_reason || :stop, usage: total_usage)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
final = extract_final_result(last_reasoning || '', last_action_result) ||
|
|
56
|
+
'Task execution reached limits without clear completion'
|
|
57
|
+
finalize(content: final, history: history, iter: iter,
|
|
58
|
+
finish_reason: :max_iterations, usage: total_usage)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def parse_and_execute_actions(reasoning, iter)
|
|
64
|
+
results = []
|
|
65
|
+
iteration_history = []
|
|
66
|
+
reasoning.scan(/USE_TOOL\[(\w+)\]\(([^)]*)\)/).each do |tool_name, params_str|
|
|
67
|
+
params = parse_tool_params(params_str)
|
|
68
|
+
tool = find_tool(tool_name)
|
|
69
|
+
|
|
70
|
+
emit(Events::ToolCallStart, iteration: iter, tool: tool_name,
|
|
71
|
+
args: params, call_id: nil)
|
|
72
|
+
|
|
73
|
+
if tool.nil?
|
|
74
|
+
err = "tool not found: #{tool_name}"
|
|
75
|
+
emit(Events::ToolCallError, iteration: iter, tool: tool_name, call_id: nil, error: err)
|
|
76
|
+
results << "Tool #{tool_name} failed: #{err}"
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
started = monotonic_ms
|
|
81
|
+
begin
|
|
82
|
+
result = tool.execute(**params)
|
|
83
|
+
duration = monotonic_ms - started
|
|
84
|
+
@agent.memory.add_tool_usage(tool_name, params, result) if @agent.respond_to?(:memory) && @agent.memory
|
|
85
|
+
emit(Events::ToolCallResult, iteration: iter, tool: tool_name,
|
|
86
|
+
call_id: nil, result: result, duration_ms: duration)
|
|
87
|
+
iteration_history << { tool: tool_name, args: params, result: result, duration_ms: duration }
|
|
88
|
+
results << "Tool #{tool_name} result: #{result}"
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
emit(Events::ToolCallError, iteration: iter, tool: tool_name,
|
|
91
|
+
call_id: nil, error: e.message)
|
|
92
|
+
results << "Tool #{tool_name} failed: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[results.join("\n"), iteration_history]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_tool_params(params_str)
|
|
100
|
+
params = {}
|
|
101
|
+
return params if params_str.strip.empty?
|
|
102
|
+
|
|
103
|
+
params_str.split(',').map(&:strip).each do |pair|
|
|
104
|
+
key, value = pair.split('=', 2).map(&:strip)
|
|
105
|
+
next unless key && value
|
|
106
|
+
|
|
107
|
+
value = value.gsub(/^["']|["']$/, '')
|
|
108
|
+
params[key.to_sym] = value
|
|
109
|
+
end
|
|
110
|
+
params
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find_tool(name)
|
|
114
|
+
@tools.find do |t|
|
|
115
|
+
t.name == name || t.class.name.split('::').last.downcase == name.downcase
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def task_complete?(reasoning, _action_result)
|
|
120
|
+
reasoning.include?('FINAL_ANSWER[') ||
|
|
121
|
+
reasoning.downcase.include?('task complete') ||
|
|
122
|
+
reasoning.downcase.include?('finished')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def extract_final_result(reasoning, action_result)
|
|
126
|
+
if (match = reasoning.match(/FINAL_ANSWER\[(.*?)\]$/m))
|
|
127
|
+
return match[1].strip
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
lines = reasoning.split("\n").map(&:strip).reject(&:empty?)
|
|
131
|
+
final_lines = lines.last(3).join(' ')
|
|
132
|
+
return final_lines if final_lines.length > 20
|
|
133
|
+
|
|
134
|
+
action_result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def emit(klass, iteration:, **attrs)
|
|
138
|
+
type_sym = klass.name.split('::').last
|
|
139
|
+
.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
|
|
140
|
+
.sub(/^_/, '').to_sym
|
|
141
|
+
@sink.call(klass.new(
|
|
142
|
+
type: type_sym,
|
|
143
|
+
timestamp: Time.now,
|
|
144
|
+
agent: @agent.respond_to?(:name) ? @agent.name : nil,
|
|
145
|
+
iteration: iteration,
|
|
146
|
+
**attrs
|
|
147
|
+
))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def accumulate_usage(total, partial)
|
|
151
|
+
return unless partial.is_a?(Hash)
|
|
152
|
+
|
|
153
|
+
total[:prompt_tokens] += partial[:prompt_tokens] || 0
|
|
154
|
+
total[:completion_tokens] += partial[:completion_tokens] || 0
|
|
155
|
+
total[:total_tokens] += partial[:total_tokens] || 0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def finalize(content:, history:, iter:, finish_reason:, usage:)
|
|
159
|
+
{
|
|
160
|
+
content: content,
|
|
161
|
+
tool_calls_history: history,
|
|
162
|
+
usage: usage,
|
|
163
|
+
iterations: iter,
|
|
164
|
+
finish_reason: finish_reason
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def monotonic_ms
|
|
169
|
+
(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/rcrewai/llm_client.rb
CHANGED