rcrewai 0.2.1 → 0.4.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +64 -1
  5. data/README.md +170 -2
  6. data/ROADMAP.md +84 -0
  7. data/Rakefile +53 -53
  8. data/bin/rcrewai +3 -3
  9. data/docs/mcp.md +109 -0
  10. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  11. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  12. data/docs/upgrading-to-0.3.md +163 -0
  13. data/examples/async_execution_example.rb +82 -81
  14. data/examples/hierarchical_crew_example.rb +68 -72
  15. data/examples/human_in_the_loop_example.rb +73 -74
  16. data/examples/mcp_example.rb +48 -0
  17. data/examples/native_tools_example.rb +64 -0
  18. data/examples/streaming_example.rb +56 -0
  19. data/lib/rcrewai/agent.rb +181 -286
  20. data/lib/rcrewai/async_executor.rb +43 -43
  21. data/lib/rcrewai/cli.rb +11 -11
  22. data/lib/rcrewai/configuration.rb +34 -9
  23. data/lib/rcrewai/crew.rb +134 -39
  24. data/lib/rcrewai/events.rb +30 -0
  25. data/lib/rcrewai/flow/state.rb +47 -0
  26. data/lib/rcrewai/flow/state_store.rb +50 -0
  27. data/lib/rcrewai/flow.rb +243 -0
  28. data/lib/rcrewai/human_input.rb +104 -114
  29. data/lib/rcrewai/knowledge/base.rb +52 -0
  30. data/lib/rcrewai/knowledge/chunker.rb +31 -0
  31. data/lib/rcrewai/knowledge/embedder.rb +48 -0
  32. data/lib/rcrewai/knowledge/sources.rb +83 -0
  33. data/lib/rcrewai/knowledge/store.rb +58 -0
  34. data/lib/rcrewai/knowledge.rb +13 -0
  35. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  36. data/lib/rcrewai/llm_client.rb +24 -1
  37. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  38. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  39. data/lib/rcrewai/llm_clients/base.rb +11 -7
  40. data/lib/rcrewai/llm_clients/google.rb +159 -95
  41. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  42. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  43. data/lib/rcrewai/mcp/client.rb +101 -0
  44. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  45. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  46. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  47. data/lib/rcrewai/mcp.rb +8 -0
  48. data/lib/rcrewai/memory.rb +45 -37
  49. data/lib/rcrewai/output_schema.rb +79 -0
  50. data/lib/rcrewai/planning.rb +65 -0
  51. data/lib/rcrewai/pricing.rb +34 -0
  52. data/lib/rcrewai/process.rb +86 -95
  53. data/lib/rcrewai/provider_schema.rb +38 -0
  54. data/lib/rcrewai/sse_parser.rb +55 -0
  55. data/lib/rcrewai/task.rb +145 -66
  56. data/lib/rcrewai/tool_runner.rb +132 -0
  57. data/lib/rcrewai/tool_schema.rb +97 -0
  58. data/lib/rcrewai/tools/base.rb +98 -37
  59. data/lib/rcrewai/tools/code_executor.rb +71 -74
  60. data/lib/rcrewai/tools/email_sender.rb +70 -78
  61. data/lib/rcrewai/tools/file_reader.rb +38 -30
  62. data/lib/rcrewai/tools/file_writer.rb +40 -38
  63. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  64. data/lib/rcrewai/tools/sql_database.rb +58 -55
  65. data/lib/rcrewai/tools/web_search.rb +26 -25
  66. data/lib/rcrewai/version.rb +2 -2
  67. data/lib/rcrewai.rb +20 -10
  68. data/rcrewai.gemspec +39 -39
  69. metadata +77 -47
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCrewAI
4
+ module Knowledge
5
+ # In-memory vector store with cosine-similarity search. The default backing
6
+ # store for Knowledge — no external service required. The interface
7
+ # (#add, #search) is intentionally small so a Chroma/Qdrant-backed store can
8
+ # be swapped in later.
9
+ class Store
10
+ Entry = Struct.new(:text, :vector)
11
+
12
+ def initialize
13
+ @entries = []
14
+ end
15
+
16
+ def add(text, vector)
17
+ @entries << Entry.new(text, vector)
18
+ end
19
+
20
+ # Returns the texts of the top-k entries most similar to +query_vector+.
21
+ def search(query_vector, k: 3)
22
+ return [] if @entries.empty?
23
+
24
+ @entries
25
+ .map { |e| [e.text, cosine_similarity(query_vector, e.vector)] }
26
+ .sort_by { |(_text, score)| -score }
27
+ .first(k)
28
+ .map(&:first)
29
+ end
30
+
31
+ def size
32
+ @entries.length
33
+ end
34
+
35
+ def empty?
36
+ @entries.empty?
37
+ end
38
+
39
+ private
40
+
41
+ def cosine_similarity(a, b)
42
+ dot = 0.0
43
+ norm_a = 0.0
44
+ norm_b = 0.0
45
+ a.each_index do |i|
46
+ ai = a[i].to_f
47
+ bi = (b[i] || 0).to_f
48
+ dot += ai * bi
49
+ norm_a += ai * ai
50
+ norm_b += bi * bi
51
+ end
52
+ return 0.0 if norm_a.zero? || norm_b.zero?
53
+
54
+ dot / (Math.sqrt(norm_a) * Math.sqrt(norm_b))
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'knowledge/chunker'
4
+ require_relative 'knowledge/store'
5
+ require_relative 'knowledge/sources'
6
+ require_relative 'knowledge/embedder'
7
+ require_relative 'knowledge/base'
8
+
9
+ module RCrewAI
10
+ # Retrieval-augmented knowledge for agents and crews. See Knowledge::Base.
11
+ module Knowledge
12
+ end
13
+ 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
@@ -28,6 +28,29 @@ module RCrewAI
28
28
  end
29
29
  end
30
30
 
31
+ # Resolves a per-agent / per-pass LLM spec into a client.
32
+ # nil -> global provider
33
+ # Symbol/String -> that provider, global model
34
+ # Hash -> { provider:, model:, api_key:, temperature: } overrides
35
+ # client object -> returned as-is (anything responding to #chat)
36
+ def self.resolve(spec, config = RCrewAI.configuration)
37
+ case spec
38
+ when nil
39
+ for_provider(nil, config)
40
+ when Symbol, String
41
+ overridden = config.with_overrides(provider: spec)
42
+ for_provider(overridden.llm_provider, overridden)
43
+ when Hash
44
+ overridden = config.with_overrides(**spec)
45
+ for_provider(overridden.llm_provider, overridden)
46
+ else
47
+ return spec if spec.respond_to?(:chat)
48
+
49
+ raise ConfigurationError,
50
+ "Invalid llm: expected a provider symbol, an options hash, or a client responding to #chat, got #{spec.class}"
51
+ end
52
+ end
53
+
31
54
  def self.chat(messages:, **options)
32
55
  client = for_provider
33
56
  client.chat(messages: messages, **options)
@@ -38,4 +61,4 @@ module RCrewAI
38
61
  client.complete(prompt: prompt, **options)
39
62
  end
40
63
  end
41
- end
64
+ end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'faraday'
4
+ require 'json'
3
5
  require_relative 'base'
6
+ require_relative '../events'
7
+ require_relative '../sse_parser'
8
+ require_relative '../provider_schema'
9
+ require_relative '../pricing'
4
10
 
5
11
  module RCrewAI
6
12
  module LLMClients
@@ -8,101 +14,215 @@ module RCrewAI
8
14
  BASE_URL = 'https://api.anthropic.com/v1'
9
15
  API_VERSION = '2023-06-01'
10
16
 
17
+ STOP_REASON_MAP = {
18
+ 'tool_use' => :tool_calls,
19
+ 'end_turn' => :stop,
20
+ 'stop_sequence' => :stop,
21
+ 'max_tokens' => :length
22
+ }.freeze
23
+
11
24
  def initialize(config = RCrewAI.configuration)
12
25
  super
13
26
  @base_url = BASE_URL
14
27
  end
15
28
 
16
- def chat(messages:, **options)
17
- # Convert messages to Anthropic format
29
+ def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options)
18
30
  system_message = extract_system_message(messages)
19
- formatted_messages = format_messages(messages.reject { |m| m.is_a?(Hash) && m[:role] == 'system' })
31
+ non_system = messages.reject { |m| m.is_a?(Hash) && m[:role] == 'system' }
20
32
 
21
33
  payload = {
22
34
  model: config.model,
23
- messages: formatted_messages,
35
+ messages: format_messages(non_system),
24
36
  max_tokens: options[:max_tokens] || config.max_tokens || 1000,
25
37
  temperature: options[:temperature] || config.temperature
26
- }
38
+ }.compact
39
+
40
+ if system_message
41
+ payload[:system] = if options[:cache_system]
42
+ [{ type: 'text', text: system_message,
43
+ cache_control: { type: 'ephemeral' } }]
44
+ else
45
+ system_message
46
+ end
47
+ end
27
48
 
28
- payload[:system] = system_message if system_message
49
+ if tools && !tools.empty?
50
+ payload[:tools] = ProviderSchema.for_many(:anthropic, tools)
51
+ payload[:tool_choice] = { type: tool_choice.to_s } if tool_choice != :auto && tool_choice.is_a?(Symbol)
52
+ end
29
53
 
30
- # Add Anthropic-specific options
31
54
  payload[:top_p] = options[:top_p] if options[:top_p]
32
55
  payload[:top_k] = options[:top_k] if options[:top_k]
33
56
  payload[:stop_sequences] = options[:stop_sequences] if options[:stop_sequences]
34
57
 
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)
58
+ if stream
59
+ payload[:stream] = true
60
+ stream_chat(payload, stream)
61
+ else
62
+ plain_chat(payload)
63
+ end
64
+ end
40
65
 
41
- result = handle_response(response)
42
- format_response(result)
66
+ def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
67
+ true
43
68
  end
44
69
 
45
70
  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'
71
+ %w[
72
+ claude-opus-4-7 claude-sonnet-4-6 claude-haiku-4-5
73
+ claude-3-5-sonnet-20241022 claude-3-haiku-20240307
54
74
  ]
55
75
  end
56
76
 
57
77
  private
58
78
 
59
- def authorization_header
79
+ def plain_chat(payload)
80
+ url = "#{@base_url}/messages"
81
+ log_request(:post, url, payload)
82
+ response = http_client.post(url, payload, build_headers.merge(auth_header))
83
+ log_response(response)
84
+ body = handle_response(response)
85
+ normalize_non_streaming(body)
86
+ end
87
+
88
+ def stream_chat(payload, sink) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
89
+ url = "#{@base_url}/messages"
90
+ log_request(:post, url, payload)
91
+
92
+ assembled_text = +''
93
+ # tool_use blocks keyed by content-block index
94
+ blocks = {}
95
+ finish_reason = nil
96
+ prompt_tokens = nil
97
+ completion_tokens = nil
98
+
99
+ parser = SSEParser.new do |sse|
100
+ data = JSON.parse(sse[:data])
101
+ case data['type']
102
+ when 'message_start'
103
+ prompt_tokens = data.dig('message', 'usage', 'input_tokens')
104
+ when 'content_block_start'
105
+ cb = data['content_block'] || {}
106
+ blocks[data['index']] = { id: cb['id'], name: cb['name'], arguments: +'' } if cb['type'] == 'tool_use'
107
+ when 'content_block_delta'
108
+ delta = data['delta'] || {}
109
+ case delta['type']
110
+ when 'text_delta'
111
+ text = delta['text'].to_s
112
+ assembled_text << text
113
+ sink.call(Events::TextDelta.new(type: :text_delta, timestamp: Time.now,
114
+ agent: nil, iteration: nil, text: text))
115
+ when 'input_json_delta'
116
+ block = blocks[data['index']]
117
+ block[:arguments] << delta['partial_json'].to_s if block
118
+ end
119
+ when 'message_delta'
120
+ finish_reason ||= STOP_REASON_MAP[data.dig('delta', 'stop_reason')] ||
121
+ data.dig('delta', 'stop_reason')&.to_sym
122
+ completion_tokens = data.dig('usage', 'output_tokens') || completion_tokens
123
+ end
124
+ end
125
+
126
+ streaming_post(url, payload) { |chunk| parser.feed(chunk) }
127
+
128
+ tool_calls = blocks.values.map do |b|
129
+ {
130
+ id: b[:id],
131
+ name: b[:name],
132
+ arguments: b[:arguments].empty? ? {} : JSON.parse(b[:arguments])
133
+ }
134
+ end
135
+
136
+ usage = {
137
+ prompt_tokens: prompt_tokens,
138
+ completion_tokens: completion_tokens,
139
+ total_tokens: (prompt_tokens || 0) + (completion_tokens || 0)
140
+ }
141
+
142
+ if prompt_tokens || completion_tokens
143
+ sink.call(Events::Usage.new(
144
+ type: :usage, timestamp: Time.now, agent: nil, iteration: nil,
145
+ prompt_tokens: prompt_tokens, completion_tokens: completion_tokens,
146
+ total_tokens: usage[:total_tokens],
147
+ cost_usd: Pricing.cost_for(config.model,
148
+ prompt_tokens: prompt_tokens || 0,
149
+ completion_tokens: completion_tokens || 0)
150
+ ))
151
+ end
152
+
60
153
  {
61
- 'x-api-key' => config.api_key,
154
+ content: assembled_text.empty? ? nil : assembled_text,
155
+ tool_calls: tool_calls,
156
+ usage: usage,
157
+ finish_reason: finish_reason || :stop,
158
+ model: config.model,
159
+ provider: :anthropic
160
+ }
161
+ end
162
+
163
+ def streaming_post(url, payload, &on_chunk)
164
+ conn = Faraday.new do |f|
165
+ f.request :json
166
+ f.options.timeout = config.timeout
167
+ f.adapter Faraday.default_adapter
168
+ end
169
+ conn.post(url) do |req|
170
+ req.headers = build_headers.merge(auth_header)
171
+ req.body = payload.to_json
172
+ req.options.on_data = proc { |chunk, _| on_chunk.call(chunk) }
173
+ end
174
+ end
175
+
176
+ def normalize_non_streaming(body)
177
+ content_blocks = Array(body['content'])
178
+ text = content_blocks.select { |b| b['type'] == 'text' }.map { |b| b['text'] }.join
179
+ tool_calls = content_blocks.select { |b| b['type'] == 'tool_use' }.map do |b|
180
+ { id: b['id'], name: b['name'], arguments: b['input'] || {} }
181
+ end
182
+ prompt_tokens = body.dig('usage', 'input_tokens')
183
+ completion_tokens = body.dig('usage', 'output_tokens')
184
+
185
+ {
186
+ content: text.empty? ? nil : text,
187
+ tool_calls: tool_calls,
188
+ usage: {
189
+ prompt_tokens: prompt_tokens,
190
+ completion_tokens: completion_tokens,
191
+ total_tokens: (prompt_tokens || 0) + (completion_tokens || 0)
192
+ },
193
+ finish_reason: STOP_REASON_MAP[body['stop_reason']] || body['stop_reason']&.to_sym || :stop,
194
+ model: body['model'] || config.model,
195
+ provider: :anthropic
196
+ }
197
+ end
198
+
199
+ def auth_header
200
+ {
201
+ 'x-api-key' => config.anthropic_api_key || config.api_key,
62
202
  'anthropic-version' => API_VERSION
63
203
  }
64
204
  end
65
205
 
66
206
  def extract_system_message(messages)
67
207
  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)
208
+
209
+ msg = messages.find { |m| m.is_a?(Hash) && m[:role] == 'system' }
210
+ msg&.dig(:content)
70
211
  end
71
212
 
72
213
  def format_messages(messages)
73
214
  messages.map do |msg|
74
215
  if msg.is_a?(Hash)
75
- {
76
- role: msg[:role] == 'assistant' ? 'assistant' : 'user',
77
- content: msg[:content]
78
- }
216
+ { role: msg[:role] == 'assistant' ? 'assistant' : 'user', content: msg[:content] }
79
217
  else
80
218
  { role: 'user', content: msg.to_s }
81
219
  end
82
220
  end
83
221
  end
84
222
 
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
223
  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
224
+ raise ConfigurationError, 'Anthropic API key is required' unless config.anthropic_api_key || config.api_key
225
+ raise ConfigurationError, 'Model is required' unless config.model
106
226
  end
107
227
 
108
228
  def handle_response(response)
@@ -110,12 +230,12 @@ module RCrewAI
110
230
  when 200..299
111
231
  response.body
112
232
  when 400
113
- error_details = response.body.dig('error', 'message') || response.body
233
+ error_details = response.body.is_a?(Hash) ? response.body.dig('error', 'message') : response.body
114
234
  raise APIError, "Bad request: #{error_details}"
115
235
  when 401
116
- raise AuthenticationError, "Invalid API key"
236
+ raise AuthenticationError, 'Invalid API key'
117
237
  when 429
118
- raise RateLimitError, "Rate limit exceeded"
238
+ raise RateLimitError, 'Rate limit exceeded'
119
239
  when 500..599
120
240
  raise APIError, "Server error: #{response.status}"
121
241
  else
@@ -124,4 +244,4 @@ module RCrewAI
124
244
  end
125
245
  end
126
246
  end
127
- end
247
+ end