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
@@ -1,158 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
3
+ require_relative 'openai'
4
4
 
5
5
  module RCrewAI
6
6
  module LLMClients
7
- class Azure < Base
7
+ # Azure OpenAI: same wire format as OpenAI but routes through a deployment
8
+ # path with an api-version query param, and authenticates with an api-key
9
+ # header instead of Authorization: Bearer.
10
+ class Azure < OpenAI
8
11
  def initialize(config = RCrewAI.configuration)
9
12
  super
10
- @base_url = config.base_url || build_azure_url
11
13
  @api_version = config.api_version || '2024-02-01'
12
14
  @deployment_name = config.deployment_name || config.model
13
- end
14
-
15
- def chat(messages:, **options)
16
- payload = {
17
- messages: format_messages(messages),
18
- temperature: options[:temperature] || config.temperature,
19
- max_tokens: options[:max_tokens] || config.max_tokens
20
- }
21
-
22
- # Add additional OpenAI-compatible options
23
- payload[:top_p] = options[:top_p] if options[:top_p]
24
- payload[:frequency_penalty] = options[:frequency_penalty] if options[:frequency_penalty]
25
- payload[:presence_penalty] = options[:presence_penalty] if options[:presence_penalty]
26
- payload[:stop] = options[:stop] if options[:stop]
27
-
28
- url = "#{@base_url}/openai/deployments/#{@deployment_name}/chat/completions?api-version=#{@api_version}"
29
- log_request(:post, url, payload)
30
-
31
- response = http_client.post(url, payload, build_headers.merge(authorization_header))
32
- log_response(response)
33
-
34
- result = handle_response(response)
35
- format_response(result)
36
- end
37
-
38
- def complete(prompt:, **options)
39
- # For older models that use completions endpoint
40
- payload = {
41
- prompt: prompt,
42
- temperature: options[:temperature] || config.temperature,
43
- max_tokens: options[:max_tokens] || config.max_tokens
44
- }
45
-
46
- url = "#{@base_url}/openai/deployments/#{@deployment_name}/completions?api-version=#{@api_version}"
47
- log_request(:post, url, payload)
48
-
49
- response = http_client.post(url, payload, build_headers.merge(authorization_header))
50
- log_response(response)
51
-
52
- result = handle_response(response)
53
- format_completion_response(result)
15
+ @base_url = build_endpoint_url
54
16
  end
55
17
 
56
18
  def models
57
- # Azure OpenAI uses deployments instead of models
58
19
  url = "#{@base_url}/openai/deployments?api-version=#{@api_version}"
59
- response = http_client.get(url, {}, build_headers.merge(authorization_header))
20
+ response = http_client.get(url, {}, build_headers.merge(auth_header))
60
21
  result = handle_response(response)
61
-
62
- if result['data']
63
- result['data'].map { |deployment| deployment['id'] }
64
- else
65
- [@deployment_name].compact
66
- end
22
+ Array(result['data']).map { |d| d['id'] }
23
+ rescue StandardError
24
+ [@deployment_name].compact
67
25
  end
68
26
 
69
27
  private
70
28
 
71
- def authorization_header
72
- { 'api-key' => config.api_key }
29
+ def chat_url
30
+ "#{@base_url}/openai/deployments/#{@deployment_name}/chat/completions?api-version=#{@api_version}"
73
31
  end
74
32
 
75
- def format_messages(messages)
76
- messages.map do |msg|
77
- if msg.is_a?(Hash)
78
- msg
79
- else
80
- { role: 'user', content: msg.to_s }
81
- end
82
- end
33
+ def build_endpoint_url
34
+ endpoint = config.base_url || ENV['AZURE_OPENAI_ENDPOINT'] || ENV['AZURE_ENDPOINT']
35
+ endpoint&.chomp('/')
83
36
  end
84
37
 
85
- def format_response(response)
86
- choice = response.dig('choices', 0)
87
- return nil unless choice
88
-
89
- {
90
- content: choice.dig('message', 'content'),
91
- role: choice.dig('message', 'role'),
92
- finish_reason: choice['finish_reason'],
93
- usage: response['usage'],
94
- model: @deployment_name,
95
- provider: :azure
96
- }
38
+ def provider_name
39
+ :azure
97
40
  end
98
41
 
99
- def format_completion_response(response)
100
- choice = response.dig('choices', 0)
101
- return nil unless choice
102
-
103
- {
104
- content: choice['text'],
105
- finish_reason: choice['finish_reason'],
106
- usage: response['usage'],
107
- model: @deployment_name,
108
- provider: :azure
109
- }
42
+ def auth_header
43
+ { 'api-key' => config.azure_api_key || config.api_key }
110
44
  end
111
45
 
112
46
  def validate_config!
113
- super
114
- raise ConfigurationError, "Azure API key is required" unless config.azure_api_key || config.api_key
115
- raise ConfigurationError, "Azure base URL or endpoint is required" unless config.base_url || azure_endpoint
116
- raise ConfigurationError, "Azure deployment name is required" unless config.deployment_name || config.model
117
- end
118
-
119
- def build_azure_url
120
- endpoint = azure_endpoint
121
- return nil unless endpoint
122
-
123
- # Remove trailing slash and add proper path
124
- endpoint = endpoint.chomp('/')
125
- "#{endpoint}"
126
- end
127
-
128
- def azure_endpoint
129
- # Try multiple environment variable names
130
- ENV['AZURE_OPENAI_ENDPOINT'] ||
131
- ENV['AZURE_ENDPOINT'] ||
132
- config.instance_variable_get(:@azure_endpoint)
133
- end
134
-
135
- def handle_response(response)
136
- case response.status
137
- when 200..299
138
- response.body
139
- when 400
140
- error_details = response.body.dig('error', 'message') || response.body
141
- raise APIError, "Bad request: #{error_details}"
142
- when 401
143
- raise AuthenticationError, "Invalid API key or authentication failed"
144
- when 403
145
- raise AuthenticationError, "Access denied - check your API key and permissions"
146
- when 404
147
- raise ModelNotFoundError, "Deployment '#{@deployment_name}' not found"
148
- when 429
149
- raise RateLimitError, "Rate limit exceeded or quota exhausted"
150
- when 500..599
151
- raise APIError, "Azure OpenAI service error: #{response.status}"
152
- else
153
- raise APIError, "Unexpected response: #{response.status}"
154
- end
47
+ raise ConfigurationError, 'Azure API key is required' unless config.azure_api_key || config.api_key
48
+ raise ConfigurationError, 'Azure endpoint is required' unless config.base_url || ENV['AZURE_OPENAI_ENDPOINT'] || ENV['AZURE_ENDPOINT']
49
+ raise ConfigurationError, 'Azure deployment name is required' unless config.deployment_name || config.model
155
50
  end
156
51
  end
157
52
  end
158
- end
53
+ end
@@ -16,8 +16,12 @@ module RCrewAI
16
16
  validate_config!
17
17
  end
18
18
 
19
- def chat(messages:, **options)
20
- raise NotImplementedError, "Subclasses must implement #chat method"
19
+ def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options)
20
+ raise NotImplementedError, 'Subclasses must implement #chat method'
21
+ end
22
+
23
+ def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
24
+ true
21
25
  end
22
26
 
23
27
  def complete(prompt:, **options)
@@ -27,8 +31,8 @@ module RCrewAI
27
31
  protected
28
32
 
29
33
  def validate_config!
30
- raise ConfigurationError, "API key is required" unless config.api_key
31
- raise ConfigurationError, "Model is required" unless config.model
34
+ raise ConfigurationError, 'API key is required' unless config.api_key
35
+ raise ConfigurationError, 'Model is required' unless config.model
32
36
  end
33
37
 
34
38
  def build_headers
@@ -54,9 +58,9 @@ module RCrewAI
54
58
  when 400
55
59
  raise APIError, "Bad request: #{response.body}"
56
60
  when 401
57
- raise AuthenticationError, "Invalid API key"
61
+ raise AuthenticationError, 'Invalid API key'
58
62
  when 429
59
- raise RateLimitError, "Rate limit exceeded"
63
+ raise RateLimitError, 'Rate limit exceeded'
60
64
  when 500..599
61
65
  raise APIError, "Server error: #{response.status}"
62
66
  else
@@ -79,4 +83,4 @@ module RCrewAI
79
83
  class RateLimitError < APIError; end
80
84
  class ModelNotFoundError < APIError; end
81
85
  end
82
- end
86
+ end
@@ -1,137 +1,201 @@
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
7
13
  class Google < Base
8
14
  BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'
9
15
 
16
+ FINISH_REASON_MAP = {
17
+ 'STOP' => :stop,
18
+ 'MAX_TOKENS' => :length,
19
+ 'SAFETY' => :stop,
20
+ 'RECITATION' => :stop
21
+ }.freeze
22
+
10
23
  def initialize(config = RCrewAI.configuration)
11
24
  super
12
25
  @base_url = BASE_URL
13
26
  end
14
27
 
15
- def chat(messages:, **options)
16
- # Convert messages to Gemini format
17
- formatted_contents = format_messages(messages)
18
-
28
+ def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options) # rubocop:disable Lint/UnusedMethodArgument
29
+ contents = format_messages(messages)
19
30
  payload = {
20
- contents: formatted_contents,
31
+ contents: contents,
21
32
  generationConfig: {
22
33
  temperature: options[:temperature] || config.temperature,
23
- maxOutputTokens: options[:max_tokens] || config.max_tokens || 2048,
24
- topP: options[:top_p] || 0.8,
25
- topK: options[:top_k] || 10
26
- }
34
+ maxOutputTokens: options[:max_tokens] || config.max_tokens || 2048
35
+ }.compact
27
36
  }
28
-
29
- # Add safety settings if provided
30
- if options[:safety_settings]
31
- payload[:safetySettings] = options[:safety_settings]
37
+ payload[:generationConfig][:topP] = options[:top_p] if options[:top_p]
38
+ payload[:generationConfig][:topK] = options[:top_k] if options[:top_k]
39
+ payload[:generationConfig][:stopSequences] = options[:stop_sequences] if options[:stop_sequences]
40
+ payload[:safetySettings] = options[:safety_settings] if options[:safety_settings]
41
+
42
+ if tools && !tools.empty?
43
+ payload[:tools] = [ProviderSchema.for_many(:google, tools)]
44
+ # tool_choice: Google has toolConfig; left as default unless explicitly set
32
45
  end
33
46
 
34
- # Add stop sequences if provided
35
- if options[:stop_sequences]
36
- payload[:generationConfig][:stopSequences] = options[:stop_sequences]
47
+ api_key = config.google_api_key || config.api_key
48
+ if stream
49
+ url = "#{@base_url}/models/#{config.model}:streamGenerateContent?alt=sse&key=#{api_key}"
50
+ stream_chat(url, payload, stream)
51
+ else
52
+ url = "#{@base_url}/models/#{config.model}:generateContent?key=#{api_key}"
53
+ plain_chat(url, payload)
37
54
  end
55
+ end
38
56
 
39
- url = "#{@base_url}/models/#{config.model}:generateContent?key=#{config.api_key}"
40
- log_request(:post, url, payload)
41
-
42
- response = http_client.post(url, payload, build_headers)
43
- log_response(response)
44
-
45
- result = handle_response(response)
46
- format_response(result)
57
+ def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
58
+ true
47
59
  end
48
60
 
49
61
  def models
50
- # Google AI Studio doesn't provide a models list endpoint with API key auth
51
- # Return known Gemini models
52
- [
53
- 'gemini-pro',
54
- 'gemini-pro-vision',
55
- 'gemini-1.5-pro',
56
- 'gemini-1.5-flash',
57
- 'text-bison-001',
58
- 'chat-bison-001'
59
- ]
62
+ %w[gemini-pro gemini-1.5-pro gemini-1.5-flash gemini-pro-vision]
60
63
  end
61
64
 
62
65
  private
63
66
 
64
- def format_messages(messages)
65
- contents = []
66
-
67
- messages.each do |msg|
68
- role = case msg[:role]
69
- when 'user'
70
- 'user'
71
- when 'assistant'
72
- 'model'
73
- when 'system'
74
- # Gemini doesn't have system role, prepend to first user message
75
- next
76
- else
77
- 'user'
78
- end
79
-
80
- content = if msg.is_a?(Hash)
81
- msg[:content]
82
- else
83
- msg.to_s
84
- end
85
-
86
- contents << {
87
- role: role,
88
- parts: [{ text: content }]
89
- }
90
- end
67
+ def plain_chat(url, payload)
68
+ log_request(:post, url, payload)
69
+ response = http_client.post(url, payload, build_headers)
70
+ log_response(response)
71
+ body = handle_response(response)
72
+ normalize_non_streaming(body)
73
+ end
74
+
75
+ def stream_chat(url, payload, sink)
76
+ log_request(:post, url, payload)
77
+
78
+ assembled_text = +''
79
+ tool_calls = []
80
+ finish_reason = nil
81
+ usage = nil
82
+
83
+ parser = SSEParser.new do |sse|
84
+ data = JSON.parse(sse[:data])
85
+ candidate = data.dig('candidates', 0) || {}
86
+ parts = candidate.dig('content', 'parts') || []
87
+
88
+ parts.each do |part|
89
+ if part['text']
90
+ text = part['text']
91
+ assembled_text << text
92
+ sink.call(Events::TextDelta.new(type: :text_delta, timestamp: Time.now,
93
+ agent: nil, iteration: nil, text: text))
94
+ elsif part['functionCall']
95
+ fc = part['functionCall']
96
+ tool_calls << { id: nil, name: fc['name'], arguments: fc['args'] || {} }
97
+ end
98
+ end
91
99
 
92
- # Handle system message by prepending to first user message
93
- system_msg = messages.find { |m| m[:role] == 'system' }
94
- if system_msg && contents.any?
95
- first_user_content = contents.find { |c| c[:role] == 'user' }
96
- if first_user_content
97
- original_text = first_user_content[:parts].first[:text]
98
- first_user_content[:parts].first[:text] = "#{system_msg[:content]}\n\n#{original_text}"
100
+ finish_reason ||= FINISH_REASON_MAP[candidate['finishReason']]
101
+ if data['usageMetadata']
102
+ usage = {
103
+ prompt_tokens: data.dig('usageMetadata', 'promptTokenCount'),
104
+ completion_tokens: data.dig('usageMetadata', 'candidatesTokenCount'),
105
+ total_tokens: data.dig('usageMetadata', 'totalTokenCount')
106
+ }
99
107
  end
100
108
  end
101
109
 
102
- contents
103
- end
110
+ streaming_post(url, payload) { |chunk| parser.feed(chunk) }
111
+
112
+ if usage
113
+ sink.call(Events::Usage.new(
114
+ type: :usage, timestamp: Time.now, agent: nil, iteration: nil,
115
+ prompt_tokens: usage[:prompt_tokens],
116
+ completion_tokens: usage[:completion_tokens],
117
+ total_tokens: usage[:total_tokens],
118
+ cost_usd: Pricing.cost_for(config.model,
119
+ prompt_tokens: usage[:prompt_tokens] || 0,
120
+ completion_tokens: usage[:completion_tokens] || 0)
121
+ ))
122
+ end
104
123
 
105
- def format_response(response)
106
- candidate = response.dig('candidates', 0)
107
- return nil unless candidate
124
+ finish_reason = :tool_calls if tool_calls.any?
108
125
 
109
- content = candidate.dig('content', 'parts', 0, 'text')
110
- finish_reason = candidate['finishReason']
126
+ {
127
+ content: assembled_text.empty? ? nil : assembled_text,
128
+ tool_calls: tool_calls,
129
+ usage: usage || {},
130
+ finish_reason: finish_reason || :stop,
131
+ model: config.model,
132
+ provider: :google
133
+ }
134
+ end
135
+
136
+ def streaming_post(url, payload, &on_chunk)
137
+ conn = Faraday.new do |f|
138
+ f.request :json
139
+ f.options.timeout = config.timeout
140
+ f.adapter Faraday.default_adapter
141
+ end
142
+ conn.post(url) do |req|
143
+ req.headers = build_headers
144
+ req.body = payload.to_json
145
+ req.options.on_data = proc { |chunk, _| on_chunk.call(chunk) }
146
+ end
147
+ end
111
148
 
112
- # Extract usage information if available
113
- usage_metadata = response['usageMetadata']
114
- usage = if usage_metadata
115
- {
116
- 'prompt_tokens' => usage_metadata['promptTokenCount'],
117
- 'completion_tokens' => usage_metadata['candidatesTokenCount'],
118
- 'total_tokens' => usage_metadata['totalTokenCount']
119
- }
120
- end
149
+ def normalize_non_streaming(body)
150
+ candidate = body.dig('candidates', 0) || {}
151
+ parts = candidate.dig('content', 'parts') || []
152
+ text = parts.select { |p| p['text'] }.map { |p| p['text'] }.join
153
+ tool_calls = parts.select { |p| p['functionCall'] }.map do |p|
154
+ { id: nil, name: p.dig('functionCall', 'name'), arguments: p.dig('functionCall', 'args') || {} }
155
+ end
156
+ usage_md = body['usageMetadata'] || {}
157
+ finish_reason = tool_calls.any? ? :tool_calls : (FINISH_REASON_MAP[candidate['finishReason']] || :stop)
121
158
 
122
159
  {
123
- content: content,
124
- role: 'assistant',
160
+ content: text.empty? ? nil : text,
161
+ tool_calls: tool_calls,
162
+ usage: {
163
+ prompt_tokens: usage_md['promptTokenCount'],
164
+ completion_tokens: usage_md['candidatesTokenCount'],
165
+ total_tokens: usage_md['totalTokenCount']
166
+ },
125
167
  finish_reason: finish_reason,
126
- usage: usage,
127
168
  model: config.model,
128
169
  provider: :google
129
170
  }
130
171
  end
131
172
 
173
+ def format_messages(messages)
174
+ contents = []
175
+ system_msg = nil
176
+
177
+ messages.each do |msg|
178
+ case msg[:role]
179
+ when 'system'
180
+ system_msg = msg[:content]
181
+ next
182
+ when 'assistant'
183
+ contents << { role: 'model', parts: [{ text: msg[:content].to_s }] }
184
+ else
185
+ contents << { role: 'user', parts: [{ text: msg[:content].to_s }] }
186
+ end
187
+ end
188
+
189
+ if system_msg && (first_user = contents.find { |c| c[:role] == 'user' })
190
+ first_user[:parts].first[:text] = "#{system_msg}\n\n#{first_user[:parts].first[:text]}"
191
+ end
192
+
193
+ contents
194
+ end
195
+
132
196
  def validate_config!
133
- super
134
- raise ConfigurationError, "Google API key is required" unless config.google_api_key || config.api_key
197
+ raise ConfigurationError, 'Google API key is required' unless config.google_api_key || config.api_key
198
+ raise ConfigurationError, 'Model is required' unless config.model
135
199
  end
136
200
 
137
201
  def handle_response(response)
@@ -139,14 +203,14 @@ module RCrewAI
139
203
  when 200..299
140
204
  response.body
141
205
  when 400
142
- error_details = response.body.dig('error', 'message') || response.body
143
- raise APIError, "Bad request: #{error_details}"
206
+ details = response.body.is_a?(Hash) ? response.body.dig('error', 'message') : response.body
207
+ raise APIError, "Bad request: #{details}"
144
208
  when 401
145
- raise AuthenticationError, "Invalid API key"
209
+ raise AuthenticationError, 'Invalid API key'
146
210
  when 403
147
- raise AuthenticationError, "API key does not have permission"
211
+ raise AuthenticationError, 'API key does not have permission'
148
212
  when 429
149
- raise RateLimitError, "Rate limit exceeded or quota exhausted"
213
+ raise RateLimitError, 'Rate limit exceeded'
150
214
  when 500..599
151
215
  raise APIError, "Server error: #{response.status}"
152
216
  else
@@ -155,4 +219,4 @@ module RCrewAI
155
219
  end
156
220
  end
157
221
  end
158
- end
222
+ end