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
@@ -0,0 +1,520 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module RCrewAI
|
8
|
+
class HumanInput
|
9
|
+
attr_reader :session_id, :interactions, :timeout, :logger
|
10
|
+
|
11
|
+
def initialize(**options)
|
12
|
+
@session_id = options[:session_id] || generate_session_id
|
13
|
+
@timeout = options.fetch(:timeout, 300) # 5 minutes default
|
14
|
+
@logger = Logger.new($stdout)
|
15
|
+
@logger.level = options.fetch(:verbose, false) ? Logger::DEBUG : Logger::INFO
|
16
|
+
@interactions = []
|
17
|
+
@input_history = []
|
18
|
+
@auto_approve = options.fetch(:auto_approve, false) # For testing/automation
|
19
|
+
@approval_keywords = options.fetch(:approval_keywords, %w[yes y approve ok continue])
|
20
|
+
@rejection_keywords = options.fetch(:rejection_keywords, %w[no n reject cancel abort])
|
21
|
+
end
|
22
|
+
|
23
|
+
def request_approval(message, **options)
|
24
|
+
interaction = create_interaction(:approval, message, options)
|
25
|
+
|
26
|
+
return handle_auto_approval(interaction) if @auto_approve
|
27
|
+
|
28
|
+
display_approval_request(interaction)
|
29
|
+
response = get_user_input(interaction)
|
30
|
+
|
31
|
+
result = process_approval_response(response, interaction)
|
32
|
+
record_interaction(interaction, response, result)
|
33
|
+
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def request_input(prompt, **options)
|
38
|
+
interaction = create_interaction(:input, prompt, options)
|
39
|
+
|
40
|
+
display_input_request(interaction)
|
41
|
+
response = get_user_input(interaction)
|
42
|
+
|
43
|
+
result = process_input_response(response, interaction)
|
44
|
+
record_interaction(interaction, response, result)
|
45
|
+
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def request_choice(prompt, choices, **options)
|
50
|
+
interaction = create_interaction(:choice, prompt, options.merge(choices: choices))
|
51
|
+
|
52
|
+
display_choice_request(interaction)
|
53
|
+
response = get_user_input(interaction)
|
54
|
+
|
55
|
+
result = process_choice_response(response, interaction)
|
56
|
+
record_interaction(interaction, response, result)
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
def request_review(content, **options)
|
62
|
+
interaction = create_interaction(:review, content, options)
|
63
|
+
|
64
|
+
display_review_request(interaction)
|
65
|
+
response = get_user_input(interaction)
|
66
|
+
|
67
|
+
result = process_review_response(response, interaction)
|
68
|
+
record_interaction(interaction, response, result)
|
69
|
+
|
70
|
+
result
|
71
|
+
end
|
72
|
+
|
73
|
+
def confirm_action(action_description, **options)
|
74
|
+
interaction = create_interaction(:confirmation, action_description, options)
|
75
|
+
|
76
|
+
return handle_auto_approval(interaction) if @auto_approve
|
77
|
+
|
78
|
+
display_confirmation_request(interaction)
|
79
|
+
response = get_user_input(interaction)
|
80
|
+
|
81
|
+
result = process_confirmation_response(response, interaction)
|
82
|
+
record_interaction(interaction, response, result)
|
83
|
+
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_feedback(prompt, **options)
|
88
|
+
interaction = create_interaction(:feedback, prompt, options)
|
89
|
+
|
90
|
+
display_feedback_request(interaction)
|
91
|
+
response = get_user_input(interaction)
|
92
|
+
|
93
|
+
result = { feedback: response, timestamp: Time.now }
|
94
|
+
record_interaction(interaction, response, result)
|
95
|
+
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
def session_summary
|
100
|
+
{
|
101
|
+
session_id: @session_id,
|
102
|
+
total_interactions: @interactions.length,
|
103
|
+
interaction_types: @interactions.group_by { |i| i[:type] }.transform_values(&:count),
|
104
|
+
approvals: @interactions.count { |i| i[:result]&.dig(:approved) },
|
105
|
+
rejections: @interactions.count { |i| i[:result]&.dig(:approved) == false },
|
106
|
+
duration: calculate_session_duration,
|
107
|
+
first_interaction: @interactions.first&.dig(:timestamp),
|
108
|
+
last_interaction: @interactions.last&.dig(:timestamp)
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def generate_session_id
|
115
|
+
"human_#{Time.now.to_i}_#{rand(1000)}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_interaction(type, content, options)
|
119
|
+
{
|
120
|
+
id: generate_interaction_id,
|
121
|
+
type: type,
|
122
|
+
content: content,
|
123
|
+
options: options,
|
124
|
+
timestamp: Time.now,
|
125
|
+
timeout: options[:timeout] || @timeout
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_interaction_id
|
130
|
+
"interaction_#{@interactions.length + 1}_#{rand(100)}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def display_approval_request(interaction)
|
134
|
+
puts "\n" + "="*60
|
135
|
+
puts "🤝 HUMAN APPROVAL REQUIRED"
|
136
|
+
puts "="*60
|
137
|
+
puts "Request: #{interaction[:content]}"
|
138
|
+
|
139
|
+
if interaction[:options][:context]
|
140
|
+
puts "\nContext: #{interaction[:options][:context]}"
|
141
|
+
end
|
142
|
+
|
143
|
+
if interaction[:options][:consequences]
|
144
|
+
puts "\nConsequences: #{interaction[:options][:consequences]}"
|
145
|
+
end
|
146
|
+
|
147
|
+
puts "\nDo you approve this action? (yes/no)"
|
148
|
+
print "> "
|
149
|
+
end
|
150
|
+
|
151
|
+
def display_input_request(interaction)
|
152
|
+
puts "\n" + "="*60
|
153
|
+
puts "💬 HUMAN INPUT REQUESTED"
|
154
|
+
puts "="*60
|
155
|
+
puts "Prompt: #{interaction[:content]}"
|
156
|
+
|
157
|
+
if interaction[:options][:help_text]
|
158
|
+
puts "\nHelp: #{interaction[:options][:help_text]}"
|
159
|
+
end
|
160
|
+
|
161
|
+
if interaction[:options][:examples]
|
162
|
+
puts "\nExamples: #{interaction[:options][:examples].join(', ')}"
|
163
|
+
end
|
164
|
+
|
165
|
+
puts "\nPlease provide your input:"
|
166
|
+
print "> "
|
167
|
+
end
|
168
|
+
|
169
|
+
def display_choice_request(interaction)
|
170
|
+
puts "\n" + "="*60
|
171
|
+
puts "🎯 HUMAN CHOICE REQUIRED"
|
172
|
+
puts "="*60
|
173
|
+
puts "Question: #{interaction[:content]}"
|
174
|
+
puts "\nAvailable choices:"
|
175
|
+
|
176
|
+
interaction[:options][:choices].each_with_index do |choice, index|
|
177
|
+
puts " #{index + 1}. #{choice}"
|
178
|
+
end
|
179
|
+
|
180
|
+
puts "\nPlease select a choice (enter number or text):"
|
181
|
+
print "> "
|
182
|
+
end
|
183
|
+
|
184
|
+
def display_review_request(interaction)
|
185
|
+
puts "\n" + "="*60
|
186
|
+
puts "👀 HUMAN REVIEW REQUESTED"
|
187
|
+
puts "="*60
|
188
|
+
puts "Content to review:"
|
189
|
+
puts "-" * 40
|
190
|
+
puts interaction[:content]
|
191
|
+
puts "-" * 40
|
192
|
+
|
193
|
+
if interaction[:options][:review_criteria]
|
194
|
+
puts "\nReview criteria: #{interaction[:options][:review_criteria].join(', ')}"
|
195
|
+
end
|
196
|
+
|
197
|
+
puts "\nPlease review and provide feedback (or type 'approve' to approve as-is):"
|
198
|
+
print "> "
|
199
|
+
end
|
200
|
+
|
201
|
+
def display_confirmation_request(interaction)
|
202
|
+
puts "\n" + "="*60
|
203
|
+
puts "⚠️ CONFIRMATION REQUIRED"
|
204
|
+
puts "="*60
|
205
|
+
puts "Action: #{interaction[:content]}"
|
206
|
+
|
207
|
+
if interaction[:options][:risk_level]
|
208
|
+
puts "Risk Level: #{interaction[:options][:risk_level]}"
|
209
|
+
end
|
210
|
+
|
211
|
+
if interaction[:options][:details]
|
212
|
+
puts "Details: #{interaction[:options][:details]}"
|
213
|
+
end
|
214
|
+
|
215
|
+
puts "\nConfirm this action? (yes/no)"
|
216
|
+
print "> "
|
217
|
+
end
|
218
|
+
|
219
|
+
def display_feedback_request(interaction)
|
220
|
+
puts "\n" + "="*60
|
221
|
+
puts "📝 FEEDBACK REQUESTED"
|
222
|
+
puts "="*60
|
223
|
+
puts "Request: #{interaction[:content]}"
|
224
|
+
|
225
|
+
puts "\nPlease provide your feedback:"
|
226
|
+
print "> "
|
227
|
+
end
|
228
|
+
|
229
|
+
def get_user_input(interaction)
|
230
|
+
start_time = Time.now
|
231
|
+
|
232
|
+
begin
|
233
|
+
if STDIN.tty?
|
234
|
+
# Interactive terminal input with timeout
|
235
|
+
response = nil
|
236
|
+
input_thread = Thread.new do
|
237
|
+
response = STDIN.gets&.chomp
|
238
|
+
end
|
239
|
+
|
240
|
+
unless input_thread.join(interaction[:timeout])
|
241
|
+
input_thread.kill
|
242
|
+
puts "\n⏰ Input timed out after #{interaction[:timeout]} seconds"
|
243
|
+
return nil
|
244
|
+
end
|
245
|
+
|
246
|
+
response
|
247
|
+
else
|
248
|
+
# Non-interactive mode (e.g., scripts, tests)
|
249
|
+
puts "⚠️ Non-interactive mode detected, using default response"
|
250
|
+
get_default_response(interaction[:type])
|
251
|
+
end
|
252
|
+
rescue Interrupt
|
253
|
+
puts "\n🛑 User interrupted input"
|
254
|
+
nil
|
255
|
+
ensure
|
256
|
+
@logger.debug "Input collection took #{(Time.now - start_time).round(2)}s"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def get_default_response(interaction_type)
|
261
|
+
case interaction_type
|
262
|
+
when :approval, :confirmation
|
263
|
+
'yes'
|
264
|
+
when :choice
|
265
|
+
'1'
|
266
|
+
when :input, :review, :feedback
|
267
|
+
'Default response - non-interactive mode'
|
268
|
+
else
|
269
|
+
'yes'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def process_approval_response(response, interaction)
|
274
|
+
return { approved: false, reason: 'No response provided' } if response.nil?
|
275
|
+
|
276
|
+
response_clean = response.strip.downcase
|
277
|
+
|
278
|
+
approved = if @approval_keywords.any? { |keyword| response_clean.include?(keyword) }
|
279
|
+
true
|
280
|
+
elsif @rejection_keywords.any? { |keyword| response_clean.include?(keyword) }
|
281
|
+
false
|
282
|
+
else
|
283
|
+
# Try to interpret ambiguous responses
|
284
|
+
response_clean.length > 0 && !response_clean.start_with?('n')
|
285
|
+
end
|
286
|
+
|
287
|
+
{
|
288
|
+
approved: approved,
|
289
|
+
response: response,
|
290
|
+
reason: approved ? 'User approved' : 'User rejected',
|
291
|
+
timestamp: Time.now
|
292
|
+
}
|
293
|
+
end
|
294
|
+
|
295
|
+
def process_input_response(response, interaction)
|
296
|
+
return { input: nil, valid: false, reason: 'No response provided' } if response.nil?
|
297
|
+
|
298
|
+
# Validate input if validation rules provided
|
299
|
+
validation_result = validate_input(response, interaction[:options][:validation] || {})
|
300
|
+
|
301
|
+
{
|
302
|
+
input: response,
|
303
|
+
valid: validation_result[:valid],
|
304
|
+
reason: validation_result[:reason],
|
305
|
+
processed_input: process_input_value(response, interaction[:options]),
|
306
|
+
timestamp: Time.now
|
307
|
+
}
|
308
|
+
end
|
309
|
+
|
310
|
+
def process_choice_response(response, interaction)
|
311
|
+
return { choice: nil, valid: false, reason: 'No response provided' } if response.nil?
|
312
|
+
|
313
|
+
choices = interaction[:options][:choices]
|
314
|
+
response_clean = response.strip
|
315
|
+
|
316
|
+
# Try to match by number
|
317
|
+
if response_clean.match?(/^\d+$/)
|
318
|
+
choice_index = response_clean.to_i - 1
|
319
|
+
if choice_index >= 0 && choice_index < choices.length
|
320
|
+
selected_choice = choices[choice_index]
|
321
|
+
return {
|
322
|
+
choice: selected_choice,
|
323
|
+
choice_index: choice_index,
|
324
|
+
valid: true,
|
325
|
+
reason: 'Valid numeric selection',
|
326
|
+
timestamp: Time.now
|
327
|
+
}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Try to match by text
|
332
|
+
selected_choice = choices.find { |choice| choice.downcase.include?(response_clean.downcase) }
|
333
|
+
if selected_choice
|
334
|
+
return {
|
335
|
+
choice: selected_choice,
|
336
|
+
choice_index: choices.index(selected_choice),
|
337
|
+
valid: true,
|
338
|
+
reason: 'Valid text selection',
|
339
|
+
timestamp: Time.now
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
# Invalid selection
|
344
|
+
{
|
345
|
+
choice: nil,
|
346
|
+
choice_index: nil,
|
347
|
+
valid: false,
|
348
|
+
reason: "Invalid selection: #{response}. Please choose from: #{choices.join(', ')}",
|
349
|
+
timestamp: Time.now
|
350
|
+
}
|
351
|
+
end
|
352
|
+
|
353
|
+
def process_review_response(response, interaction)
|
354
|
+
return { feedback: nil, approved: false, reason: 'No response provided' } if response.nil?
|
355
|
+
|
356
|
+
response_clean = response.strip.downcase
|
357
|
+
|
358
|
+
# Check if it's a simple approval
|
359
|
+
if @approval_keywords.any? { |keyword| response_clean == keyword }
|
360
|
+
return {
|
361
|
+
feedback: response,
|
362
|
+
approved: true,
|
363
|
+
reason: 'Content approved without changes',
|
364
|
+
timestamp: Time.now
|
365
|
+
}
|
366
|
+
end
|
367
|
+
|
368
|
+
# Otherwise treat as feedback
|
369
|
+
{
|
370
|
+
feedback: response,
|
371
|
+
approved: false,
|
372
|
+
reason: 'Feedback provided for review',
|
373
|
+
suggested_changes: extract_suggestions(response),
|
374
|
+
timestamp: Time.now
|
375
|
+
}
|
376
|
+
end
|
377
|
+
|
378
|
+
def process_confirmation_response(response, interaction)
|
379
|
+
process_approval_response(response, interaction)
|
380
|
+
end
|
381
|
+
|
382
|
+
def validate_input(input, validation_rules)
|
383
|
+
return { valid: true, reason: 'No validation rules' } if validation_rules.empty?
|
384
|
+
|
385
|
+
# Check minimum length
|
386
|
+
if validation_rules[:min_length] && input.length < validation_rules[:min_length]
|
387
|
+
return { valid: false, reason: "Input too short (minimum #{validation_rules[:min_length]} characters)" }
|
388
|
+
end
|
389
|
+
|
390
|
+
# Check maximum length
|
391
|
+
if validation_rules[:max_length] && input.length > validation_rules[:max_length]
|
392
|
+
return { valid: false, reason: "Input too long (maximum #{validation_rules[:max_length]} characters)" }
|
393
|
+
end
|
394
|
+
|
395
|
+
# Check pattern matching
|
396
|
+
if validation_rules[:pattern] && !input.match?(validation_rules[:pattern])
|
397
|
+
return { valid: false, reason: "Input doesn't match required pattern" }
|
398
|
+
end
|
399
|
+
|
400
|
+
# Check required keywords
|
401
|
+
if validation_rules[:required_keywords]
|
402
|
+
missing_keywords = validation_rules[:required_keywords].reject { |keyword| input.downcase.include?(keyword.downcase) }
|
403
|
+
unless missing_keywords.empty?
|
404
|
+
return { valid: false, reason: "Missing required keywords: #{missing_keywords.join(', ')}" }
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
{ valid: true, reason: 'Input passes validation' }
|
409
|
+
end
|
410
|
+
|
411
|
+
def process_input_value(input, options)
|
412
|
+
return input unless options[:type]
|
413
|
+
|
414
|
+
case options[:type]
|
415
|
+
when :integer
|
416
|
+
input.to_i
|
417
|
+
when :float
|
418
|
+
input.to_f
|
419
|
+
when :boolean
|
420
|
+
%w[true yes 1 y].include?(input.downcase)
|
421
|
+
when :json
|
422
|
+
begin
|
423
|
+
JSON.parse(input)
|
424
|
+
rescue JSON::ParserError
|
425
|
+
input
|
426
|
+
end
|
427
|
+
else
|
428
|
+
input
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def extract_suggestions(feedback)
|
433
|
+
suggestions = []
|
434
|
+
|
435
|
+
# Simple pattern matching for common suggestion indicators
|
436
|
+
suggestion_patterns = [
|
437
|
+
/(?:should|could|might want to|consider|suggest|recommend)\s+(.+?)(?:\.|$)/i,
|
438
|
+
/(?:change|modify|update|fix|improve)\s+(.+?)(?:\.|$)/i,
|
439
|
+
/(?:add|include|remove|delete)\s+(.+?)(?:\.|$)/i
|
440
|
+
]
|
441
|
+
|
442
|
+
suggestion_patterns.each do |pattern|
|
443
|
+
matches = feedback.scan(pattern)
|
444
|
+
suggestions.concat(matches.flatten)
|
445
|
+
end
|
446
|
+
|
447
|
+
suggestions.map(&:strip).reject(&:empty?).uniq
|
448
|
+
end
|
449
|
+
|
450
|
+
def handle_auto_approval(interaction)
|
451
|
+
@logger.debug "Auto-approving interaction: #{interaction[:id]}"
|
452
|
+
{
|
453
|
+
approved: true,
|
454
|
+
response: 'auto-approved',
|
455
|
+
reason: 'Auto-approval enabled',
|
456
|
+
timestamp: Time.now
|
457
|
+
}
|
458
|
+
end
|
459
|
+
|
460
|
+
def record_interaction(interaction, response, result)
|
461
|
+
interaction[:response] = response
|
462
|
+
interaction[:result] = result
|
463
|
+
interaction[:completed_at] = Time.now
|
464
|
+
interaction[:duration] = interaction[:completed_at] - interaction[:timestamp]
|
465
|
+
|
466
|
+
@interactions << interaction
|
467
|
+
@input_history << {
|
468
|
+
type: interaction[:type],
|
469
|
+
response: response,
|
470
|
+
timestamp: Time.now
|
471
|
+
}
|
472
|
+
|
473
|
+
@logger.info "Human interaction completed: #{interaction[:type]} - #{result[:reason] || 'Success'}"
|
474
|
+
end
|
475
|
+
|
476
|
+
def calculate_session_duration
|
477
|
+
return 0 if @interactions.empty?
|
478
|
+
|
479
|
+
first = @interactions.first[:timestamp]
|
480
|
+
last = @interactions.last[:completed_at] || @interactions.last[:timestamp]
|
481
|
+
last - first
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Extensions for agents and tasks to support human input
|
486
|
+
module HumanInteractionExtensions
|
487
|
+
def request_human_approval(message, **options)
|
488
|
+
human_input_client.request_approval(message, **options)
|
489
|
+
end
|
490
|
+
|
491
|
+
def request_human_input(prompt, **options)
|
492
|
+
human_input_client.request_input(prompt, **options)
|
493
|
+
end
|
494
|
+
|
495
|
+
def request_human_choice(prompt, choices, **options)
|
496
|
+
human_input_client.request_choice(prompt, choices, **options)
|
497
|
+
end
|
498
|
+
|
499
|
+
def request_human_review(content, **options)
|
500
|
+
human_input_client.request_review(content, **options)
|
501
|
+
end
|
502
|
+
|
503
|
+
def confirm_with_human(action, **options)
|
504
|
+
human_input_client.confirm_action(action, **options)
|
505
|
+
end
|
506
|
+
|
507
|
+
def get_human_feedback(prompt, **options)
|
508
|
+
human_input_client.get_feedback(prompt, **options)
|
509
|
+
end
|
510
|
+
|
511
|
+
private
|
512
|
+
|
513
|
+
def human_input_client
|
514
|
+
@human_input_client ||= HumanInput.new(
|
515
|
+
session_id: "#{self.class.name.downcase}_#{name}",
|
516
|
+
verbose: respond_to?(:verbose) ? verbose : false
|
517
|
+
)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'llm_clients/base'
|
4
|
+
require_relative 'llm_clients/openai'
|
5
|
+
require_relative 'llm_clients/anthropic'
|
6
|
+
require_relative 'llm_clients/google'
|
7
|
+
require_relative 'llm_clients/azure'
|
8
|
+
require_relative 'llm_clients/ollama'
|
9
|
+
|
10
|
+
module RCrewAI
|
11
|
+
class LLMClient
|
12
|
+
def self.for_provider(provider = nil, config = RCrewAI.configuration)
|
13
|
+
provider ||= config.llm_provider
|
14
|
+
|
15
|
+
case provider.to_sym
|
16
|
+
when :openai
|
17
|
+
LLMClients::OpenAI.new(config)
|
18
|
+
when :anthropic
|
19
|
+
LLMClients::Anthropic.new(config)
|
20
|
+
when :google
|
21
|
+
LLMClients::Google.new(config)
|
22
|
+
when :azure
|
23
|
+
LLMClients::Azure.new(config)
|
24
|
+
when :ollama
|
25
|
+
LLMClients::Ollama.new(config)
|
26
|
+
else
|
27
|
+
raise ConfigurationError, "Unsupported provider: #{provider}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.chat(messages:, **options)
|
32
|
+
client = for_provider
|
33
|
+
client.chat(messages: messages, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.complete(prompt:, **options)
|
37
|
+
client = for_provider
|
38
|
+
client.complete(prompt: prompt, **options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RCrewAI
|
6
|
+
module LLMClients
|
7
|
+
class Anthropic < Base
|
8
|
+
BASE_URL = 'https://api.anthropic.com/v1'
|
9
|
+
API_VERSION = '2023-06-01'
|
10
|
+
|
11
|
+
def initialize(config = RCrewAI.configuration)
|
12
|
+
super
|
13
|
+
@base_url = BASE_URL
|
14
|
+
end
|
15
|
+
|
16
|
+
def chat(messages:, **options)
|
17
|
+
# Convert messages to Anthropic format
|
18
|
+
system_message = extract_system_message(messages)
|
19
|
+
formatted_messages = format_messages(messages.reject { |m| m.is_a?(Hash) && m[:role] == 'system' })
|
20
|
+
|
21
|
+
payload = {
|
22
|
+
model: config.model,
|
23
|
+
messages: formatted_messages,
|
24
|
+
max_tokens: options[:max_tokens] || config.max_tokens || 1000,
|
25
|
+
temperature: options[:temperature] || config.temperature
|
26
|
+
}
|
27
|
+
|
28
|
+
payload[:system] = system_message if system_message
|
29
|
+
|
30
|
+
# Add Anthropic-specific options
|
31
|
+
payload[:top_p] = options[:top_p] if options[:top_p]
|
32
|
+
payload[:top_k] = options[:top_k] if options[:top_k]
|
33
|
+
payload[:stop_sequences] = options[:stop_sequences] if options[:stop_sequences]
|
34
|
+
|
35
|
+
url = "#{@base_url}/messages"
|
36
|
+
log_request(:post, url, payload)
|
37
|
+
|
38
|
+
response = http_client.post(url, payload, build_headers.merge(authorization_header))
|
39
|
+
log_response(response)
|
40
|
+
|
41
|
+
result = handle_response(response)
|
42
|
+
format_response(result)
|
43
|
+
end
|
44
|
+
|
45
|
+
def models
|
46
|
+
# Anthropic doesn't have a models endpoint, return known models
|
47
|
+
[
|
48
|
+
'claude-3-opus-20240229',
|
49
|
+
'claude-3-sonnet-20240229',
|
50
|
+
'claude-3-haiku-20240307',
|
51
|
+
'claude-2.1',
|
52
|
+
'claude-2.0',
|
53
|
+
'claude-instant-1.2'
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def authorization_header
|
60
|
+
{
|
61
|
+
'Authorization' => "Bearer #{config.api_key}",
|
62
|
+
'anthropic-version' => API_VERSION
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def extract_system_message(messages)
|
67
|
+
return nil unless messages.is_a?(Array)
|
68
|
+
system_msg = messages.find { |m| m.is_a?(Hash) && m[:role] == 'system' }
|
69
|
+
system_msg&.dig(:content)
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_messages(messages)
|
73
|
+
messages.map do |msg|
|
74
|
+
if msg.is_a?(Hash)
|
75
|
+
{
|
76
|
+
role: msg[:role] == 'assistant' ? 'assistant' : 'user',
|
77
|
+
content: msg[:content]
|
78
|
+
}
|
79
|
+
else
|
80
|
+
{ role: 'user', content: msg.to_s }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def format_response(response)
|
86
|
+
content = response.dig('content', 0, 'text') if response['content']&.any?
|
87
|
+
|
88
|
+
{
|
89
|
+
content: content,
|
90
|
+
role: 'assistant',
|
91
|
+
finish_reason: response['stop_reason'],
|
92
|
+
usage: {
|
93
|
+
'prompt_tokens' => response.dig('usage', 'input_tokens'),
|
94
|
+
'completion_tokens' => response.dig('usage', 'output_tokens'),
|
95
|
+
'total_tokens' => (response.dig('usage', 'input_tokens') || 0) +
|
96
|
+
(response.dig('usage', 'output_tokens') || 0)
|
97
|
+
},
|
98
|
+
model: response['model'],
|
99
|
+
provider: :anthropic
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate_config!
|
104
|
+
raise ConfigurationError, "Anthropic API key is required" unless config.anthropic_api_key || config.api_key
|
105
|
+
raise ConfigurationError, "Model is required" unless config.model
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_response(response)
|
109
|
+
case response.status
|
110
|
+
when 200..299
|
111
|
+
response.body
|
112
|
+
when 400
|
113
|
+
error_details = response.body.dig('error', 'message') || response.body
|
114
|
+
raise APIError, "Bad request: #{error_details}"
|
115
|
+
when 401
|
116
|
+
raise AuthenticationError, "Invalid API key"
|
117
|
+
when 429
|
118
|
+
raise RateLimitError, "Rate limit exceeded"
|
119
|
+
when 500..599
|
120
|
+
raise APIError, "Server error: #{response.status}"
|
121
|
+
else
|
122
|
+
raise APIError, "Unexpected response: #{response.status}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|