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,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