rcrewai 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +99 -0
- data/CHANGELOG.md +24 -0
- data/README.md +33 -1
- data/Rakefile +53 -53
- data/bin/rcrewai +3 -3
- data/docs/mcp.md +109 -0
- data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
- data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
- data/docs/upgrading-to-0.3.md +163 -0
- data/examples/async_execution_example.rb +82 -81
- data/examples/hierarchical_crew_example.rb +68 -72
- data/examples/human_in_the_loop_example.rb +73 -74
- data/examples/mcp_example.rb +48 -0
- data/examples/native_tools_example.rb +64 -0
- data/examples/streaming_example.rb +56 -0
- data/lib/rcrewai/agent.rb +148 -287
- data/lib/rcrewai/async_executor.rb +43 -43
- data/lib/rcrewai/cli.rb +11 -11
- data/lib/rcrewai/configuration.rb +14 -9
- data/lib/rcrewai/crew.rb +56 -39
- data/lib/rcrewai/events.rb +30 -0
- data/lib/rcrewai/human_input.rb +104 -114
- data/lib/rcrewai/legacy_react_runner.rb +172 -0
- data/lib/rcrewai/llm_client.rb +1 -1
- data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
- data/lib/rcrewai/llm_clients/azure.rb +23 -128
- data/lib/rcrewai/llm_clients/base.rb +11 -7
- data/lib/rcrewai/llm_clients/google.rb +159 -95
- data/lib/rcrewai/llm_clients/ollama.rb +150 -106
- data/lib/rcrewai/llm_clients/openai.rb +140 -63
- data/lib/rcrewai/mcp/client.rb +101 -0
- data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
- data/lib/rcrewai/mcp/transport/http.rb +53 -0
- data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
- data/lib/rcrewai/mcp.rb +8 -0
- data/lib/rcrewai/memory.rb +45 -37
- data/lib/rcrewai/pricing.rb +34 -0
- data/lib/rcrewai/process.rb +86 -95
- data/lib/rcrewai/provider_schema.rb +38 -0
- data/lib/rcrewai/sse_parser.rb +55 -0
- data/lib/rcrewai/task.rb +56 -64
- data/lib/rcrewai/tool_runner.rb +132 -0
- data/lib/rcrewai/tool_schema.rb +97 -0
- data/lib/rcrewai/tools/base.rb +98 -37
- data/lib/rcrewai/tools/code_executor.rb +71 -74
- data/lib/rcrewai/tools/email_sender.rb +70 -78
- data/lib/rcrewai/tools/file_reader.rb +38 -30
- data/lib/rcrewai/tools/file_writer.rb +40 -38
- data/lib/rcrewai/tools/pdf_processor.rb +115 -130
- data/lib/rcrewai/tools/sql_database.rb +58 -55
- data/lib/rcrewai/tools/web_search.rb +26 -25
- data/lib/rcrewai/version.rb +2 -2
- data/lib/rcrewai.rb +18 -10
- data/rcrewai.gemspec +55 -36
- metadata +86 -50
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
66
|
+
def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
|
|
67
|
+
true
|
|
43
68
|
end
|
|
44
69
|
|
|
45
70
|
def models
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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,
|
|
105
|
-
raise ConfigurationError,
|
|
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')
|
|
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,
|
|
236
|
+
raise AuthenticationError, 'Invalid API key'
|
|
117
237
|
when 429
|
|
118
|
-
raise RateLimitError,
|
|
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
|
|
@@ -1,158 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'openai'
|
|
4
4
|
|
|
5
5
|
module RCrewAI
|
|
6
6
|
module LLMClients
|
|
7
|
-
|
|
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
|
-
|
|
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(
|
|
20
|
+
response = http_client.get(url, {}, build_headers.merge(auth_header))
|
|
60
21
|
result = handle_response(response)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
72
|
-
{
|
|
29
|
+
def chat_url
|
|
30
|
+
"#{@base_url}/openai/deployments/#{@deployment_name}/chat/completions?api-version=#{@api_version}"
|
|
73
31
|
end
|
|
74
32
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
86
|
-
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
114
|
-
raise ConfigurationError,
|
|
115
|
-
raise ConfigurationError,
|
|
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,
|
|
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,
|
|
31
|
-
raise ConfigurationError,
|
|
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,
|
|
61
|
+
raise AuthenticationError, 'Invalid API key'
|
|
58
62
|
when 429
|
|
59
|
-
raise RateLimitError,
|
|
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
|