rcrewai 0.7.0 → 0.8.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/CHANGELOG.md +51 -1
- data/ROADMAP.md +144 -75
- data/docs/api/agent.md +8 -0
- data/docs/api/crew.md +53 -3
- data/docs/api/index.md +14 -13
- data/docs/api/task.md +7 -0
- data/docs/examples/async-execution.md +8 -5
- data/docs/examples/tool-composition.md +3 -3
- data/docs/index.md +22 -7
- data/docs/tutorials/agent-options.md +128 -0
- data/docs/tutorials/consensual-process.md +58 -0
- data/docs/tutorials/flows.md +135 -0
- data/docs/tutorials/index.md +45 -0
- data/docs/tutorials/knowledge.md +80 -0
- data/docs/tutorials/memory.md +90 -0
- data/lib/rcrewai/checkpoint/cli.rb +98 -0
- data/lib/rcrewai/checkpoint.rb +145 -0
- data/lib/rcrewai/cli.rb +12 -1
- data/lib/rcrewai/configuration.rb +7 -0
- data/lib/rcrewai/crew.rb +88 -2
- data/lib/rcrewai/events.rb +71 -5
- data/lib/rcrewai/legacy_react_runner.rb +14 -9
- data/lib/rcrewai/llm_client.rb +20 -15
- data/lib/rcrewai/llm_clients/anthropic.rb +13 -4
- data/lib/rcrewai/llm_clients/azure.rb +2 -2
- data/lib/rcrewai/llm_clients/base.rb +55 -1
- data/lib/rcrewai/llm_clients/bedrock.rb +134 -0
- data/lib/rcrewai/llm_clients/google.rb +13 -4
- data/lib/rcrewai/llm_clients/ollama.rb +18 -4
- data/lib/rcrewai/llm_clients/openai.rb +19 -15
- data/lib/rcrewai/llm_clients/openai_compatible.rb +34 -0
- data/lib/rcrewai/llm_clients/openai_responses.rb +127 -0
- data/lib/rcrewai/llm_clients/snowflake_cortex.rb +42 -0
- data/lib/rcrewai/process.rb +41 -2
- data/lib/rcrewai/task.rb +3 -2
- data/lib/rcrewai/tool_runner.rb +16 -10
- data/lib/rcrewai/version.rb +1 -1
- data/lib/rcrewai.rb +3 -0
- metadata +12 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative 'base'
|
|
7
|
+
require_relative '../events'
|
|
8
|
+
require_relative '../pricing'
|
|
9
|
+
|
|
10
|
+
module RCrewAI
|
|
11
|
+
module LLMClients
|
|
12
|
+
# AWS Bedrock via the Converse API (v4). Converse gives every Bedrock model
|
|
13
|
+
# one request/response shape regardless of the underlying vendor, so this
|
|
14
|
+
# client speaks Converse rather than each model's native format.
|
|
15
|
+
#
|
|
16
|
+
# Authentication: the configured api_key is sent as a bearer token, which
|
|
17
|
+
# covers Bedrock API keys and any gateway fronting Bedrock. Full SigV4
|
|
18
|
+
# request signing is not implemented -- it needs the aws-sigv4 gem, and
|
|
19
|
+
# adding a hard AWS dependency for one provider is not worth it. Users
|
|
20
|
+
# needing SigV4 can sign via a before_request hook.
|
|
21
|
+
class Bedrock < Base
|
|
22
|
+
STOP_REASONS = {
|
|
23
|
+
'end_turn' => :stop,
|
|
24
|
+
'stop_sequence' => :stop,
|
|
25
|
+
'max_tokens' => :length,
|
|
26
|
+
'tool_use' => :tool_calls,
|
|
27
|
+
'content_filtered' => :content_filter
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
31
|
+
super
|
|
32
|
+
@region = config.aws_region
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def provider_name
|
|
36
|
+
:bedrock
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options) # rubocop:disable Lint/UnusedMethodArgument
|
|
40
|
+
system_text = extract_system(messages)
|
|
41
|
+
payload = {
|
|
42
|
+
messages: format_messages(messages),
|
|
43
|
+
inferenceConfig: {
|
|
44
|
+
temperature: options[:temperature] || config.temperature,
|
|
45
|
+
maxTokens: options[:max_tokens] || config.max_tokens
|
|
46
|
+
}.compact
|
|
47
|
+
}
|
|
48
|
+
payload[:system] = [{ text: system_text }] if system_text
|
|
49
|
+
payload[:toolConfig] = { tools: format_tools(tools) } if tools && !tools.empty?
|
|
50
|
+
|
|
51
|
+
plain_chat(payload)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Converse exposes tool use uniformly, but streaming uses a separate
|
|
55
|
+
# endpoint and event-stream framing that this client does not implement.
|
|
56
|
+
def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def plain_chat(payload)
|
|
63
|
+
url = converse_url
|
|
64
|
+
payload = apply_before_request(payload)
|
|
65
|
+
started_at = Time.now
|
|
66
|
+
log_request(:post, url, payload)
|
|
67
|
+
response = http_client.post(url, payload, build_headers.merge(auth_header))
|
|
68
|
+
log_response(response)
|
|
69
|
+
body = handle_response(response)
|
|
70
|
+
apply_after_response(normalize(body), started_at)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def converse_url
|
|
74
|
+
"https://bedrock-runtime.#{@region}.amazonaws.com/model/#{CGI.escape(config.model)}/converse"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Converse carries the system prompt at the top level, not in messages.
|
|
78
|
+
def extract_system(messages)
|
|
79
|
+
systems = messages.select { |m| m.is_a?(Hash) && m[:role].to_s == 'system' }
|
|
80
|
+
return nil if systems.empty?
|
|
81
|
+
|
|
82
|
+
systems.map { |m| m[:content] }.join("\n\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Every message content is a list of typed blocks.
|
|
86
|
+
def format_messages(messages)
|
|
87
|
+
messages.reject { |m| m.is_a?(Hash) && m[:role].to_s == 'system' }.map do |m|
|
|
88
|
+
{ role: m[:role].to_s, content: [{ text: m[:content].to_s }] }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_tools(tools)
|
|
93
|
+
tools.map do |t|
|
|
94
|
+
{ toolSpec: { name: t[:name], description: t[:description],
|
|
95
|
+
inputSchema: { json: t[:parameters] } } }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def normalize(body)
|
|
100
|
+
blocks = body.dig('output', 'message', 'content') || []
|
|
101
|
+
text = blocks.filter_map { |b| b['text'] }.join
|
|
102
|
+
tool_calls = blocks.filter_map do |b|
|
|
103
|
+
use = b['toolUse']
|
|
104
|
+
next unless use
|
|
105
|
+
|
|
106
|
+
{ id: use['toolUseId'], name: use['name'], arguments: use['input'] || {} }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
content: text.empty? ? nil : text,
|
|
111
|
+
tool_calls: tool_calls,
|
|
112
|
+
usage: {
|
|
113
|
+
prompt_tokens: body.dig('usage', 'inputTokens'),
|
|
114
|
+
completion_tokens: body.dig('usage', 'outputTokens'),
|
|
115
|
+
total_tokens: body.dig('usage', 'totalTokens')
|
|
116
|
+
},
|
|
117
|
+
finish_reason: STOP_REASONS.fetch(body['stopReason'], :stop),
|
|
118
|
+
model: config.model,
|
|
119
|
+
provider: provider_name
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def auth_header
|
|
124
|
+
{ 'Authorization' => "Bearer #{config.api_key}" }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_config!
|
|
128
|
+
raise ConfigurationError, 'Bedrock API key is required' unless config.api_key
|
|
129
|
+
raise ConfigurationError, 'An AWS region is required for Bedrock' unless config.aws_region
|
|
130
|
+
raise ConfigurationError, 'Model is required' unless config.model
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -20,8 +20,8 @@ module RCrewAI
|
|
|
20
20
|
'RECITATION' => :stop
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
|
-
def initialize(config = RCrewAI.configuration)
|
|
24
|
-
super
|
|
23
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
24
|
+
super(config, **hooks)
|
|
25
25
|
@base_url = BASE_URL
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -62,17 +62,25 @@ module RCrewAI
|
|
|
62
62
|
%w[gemini-pro gemini-1.5-pro gemini-1.5-flash gemini-pro-vision]
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def provider_name
|
|
66
|
+
:google
|
|
67
|
+
end
|
|
68
|
+
|
|
65
69
|
private
|
|
66
70
|
|
|
67
71
|
def plain_chat(url, payload)
|
|
72
|
+
payload = apply_before_request(payload)
|
|
73
|
+
started_at = Time.now
|
|
68
74
|
log_request(:post, url, payload)
|
|
69
75
|
response = http_client.post(url, payload, build_headers)
|
|
70
76
|
log_response(response)
|
|
71
77
|
body = handle_response(response)
|
|
72
|
-
normalize_non_streaming(body)
|
|
78
|
+
apply_after_response(normalize_non_streaming(body), started_at)
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
def stream_chat(url, payload, sink)
|
|
82
|
+
payload = apply_before_request(payload)
|
|
83
|
+
started_at = Time.now
|
|
76
84
|
log_request(:post, url, payload)
|
|
77
85
|
|
|
78
86
|
assembled_text = +''
|
|
@@ -123,7 +131,7 @@ module RCrewAI
|
|
|
123
131
|
|
|
124
132
|
finish_reason = :tool_calls if tool_calls.any?
|
|
125
133
|
|
|
126
|
-
{
|
|
134
|
+
result = {
|
|
127
135
|
content: assembled_text.empty? ? nil : assembled_text,
|
|
128
136
|
tool_calls: tool_calls,
|
|
129
137
|
usage: usage || {},
|
|
@@ -131,6 +139,7 @@ module RCrewAI
|
|
|
131
139
|
model: config.model,
|
|
132
140
|
provider: :google
|
|
133
141
|
}
|
|
142
|
+
apply_after_response(result, started_at)
|
|
134
143
|
end
|
|
135
144
|
|
|
136
145
|
def streaming_post(url, payload, &on_chunk)
|
|
@@ -21,8 +21,8 @@ module RCrewAI
|
|
|
21
21
|
firefunction-v2
|
|
22
22
|
].freeze
|
|
23
23
|
|
|
24
|
-
def initialize(config = RCrewAI.configuration)
|
|
25
|
-
super
|
|
24
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
25
|
+
super(config, **hooks)
|
|
26
26
|
@base_url = config.base_url || ollama_url || DEFAULT_URL
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -78,17 +78,25 @@ module RCrewAI
|
|
|
78
78
|
handle_response(response)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
def provider_name
|
|
82
|
+
:ollama
|
|
83
|
+
end
|
|
84
|
+
|
|
81
85
|
private
|
|
82
86
|
|
|
83
87
|
def plain_chat(url, payload)
|
|
88
|
+
payload = apply_before_request(payload)
|
|
89
|
+
started_at = Time.now
|
|
84
90
|
log_request(:post, url, payload)
|
|
85
91
|
response = http_client.post(url, payload, build_headers)
|
|
86
92
|
log_response(response)
|
|
87
93
|
body = handle_response(response)
|
|
88
|
-
normalize_non_streaming(body)
|
|
94
|
+
apply_after_response(normalize_non_streaming(body), started_at)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
def stream_chat(url, payload, sink)
|
|
98
|
+
payload = apply_before_request(payload)
|
|
99
|
+
started_at = Time.now
|
|
92
100
|
log_request(:post, url, payload)
|
|
93
101
|
|
|
94
102
|
assembled_text = +''
|
|
@@ -145,8 +153,14 @@ module RCrewAI
|
|
|
145
153
|
))
|
|
146
154
|
end
|
|
147
155
|
|
|
156
|
+
result = stream_result(assembled_text, tool_calls, finish_reason,
|
|
157
|
+
prompt_tokens, completion_tokens)
|
|
158
|
+
apply_after_response(result, started_at)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def stream_result(text, tool_calls, finish_reason, prompt_tokens, completion_tokens)
|
|
148
162
|
{
|
|
149
|
-
content:
|
|
163
|
+
content: text.empty? ? nil : text,
|
|
150
164
|
tool_calls: tool_calls,
|
|
151
165
|
usage: {
|
|
152
166
|
prompt_tokens: prompt_tokens,
|
|
@@ -13,8 +13,8 @@ module RCrewAI
|
|
|
13
13
|
class OpenAI < Base
|
|
14
14
|
BASE_URL = 'https://api.openai.com/v1'
|
|
15
15
|
|
|
16
|
-
def initialize(config = RCrewAI.configuration)
|
|
17
|
-
super
|
|
16
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
17
|
+
super(config, **hooks)
|
|
18
18
|
@base_url = BASE_URL
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -49,6 +49,10 @@ module RCrewAI
|
|
|
49
49
|
true
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def provider_name
|
|
53
|
+
:openai
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
def models
|
|
53
57
|
url = "#{@base_url}/models"
|
|
54
58
|
response = http_client.get(url, {}, build_headers.merge(auth_header))
|
|
@@ -64,15 +68,19 @@ module RCrewAI
|
|
|
64
68
|
|
|
65
69
|
def plain_chat(payload)
|
|
66
70
|
url = chat_url
|
|
71
|
+
payload = apply_before_request(payload)
|
|
72
|
+
started_at = Time.now
|
|
67
73
|
log_request(:post, url, payload)
|
|
68
74
|
response = http_client.post(url, payload, build_headers.merge(auth_header))
|
|
69
75
|
log_response(response)
|
|
70
76
|
body = handle_response(response)
|
|
71
|
-
normalize_non_streaming(body)
|
|
77
|
+
apply_after_response(normalize_non_streaming(body), started_at)
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
def stream_chat(payload, sink) # rubocop:disable Metrics/AbcSize
|
|
75
81
|
url = chat_url
|
|
82
|
+
payload = apply_before_request(payload)
|
|
83
|
+
started_at = Time.now
|
|
76
84
|
log_request(:post, url, payload)
|
|
77
85
|
|
|
78
86
|
assembled_text = +''
|
|
@@ -137,18 +145,14 @@ module RCrewAI
|
|
|
137
145
|
))
|
|
138
146
|
end
|
|
139
147
|
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def provider_name
|
|
151
|
-
:openai
|
|
148
|
+
apply_after_response({
|
|
149
|
+
content: assembled_text.empty? ? nil : assembled_text,
|
|
150
|
+
tool_calls: tool_calls,
|
|
151
|
+
usage: final_usage || {},
|
|
152
|
+
finish_reason: finish_reason || :stop,
|
|
153
|
+
model: config.model,
|
|
154
|
+
provider: provider_name
|
|
155
|
+
}, started_at)
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
def streaming_post(url, payload, &on_chunk)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'openai'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
module LLMClients
|
|
7
|
+
# Any endpoint speaking the OpenAI Chat Completions wire format: Together,
|
|
8
|
+
# Groq, Fireworks, vLLM, LiteLLM, OpenRouter, a self-hosted gateway. Only
|
|
9
|
+
# the base URL differs, so this is OpenAI with the endpoint made explicit
|
|
10
|
+
# and required.
|
|
11
|
+
class OpenAICompatible < OpenAI
|
|
12
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
13
|
+
super
|
|
14
|
+
@base_url = config.base_url.chomp('/')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def provider_name
|
|
18
|
+
:openai_compatible
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate_config!
|
|
24
|
+
raise ConfigurationError, 'API key is required' unless config.api_key
|
|
25
|
+
raise ConfigurationError, 'A base url is required for an OpenAI-compatible provider' unless config.base_url
|
|
26
|
+
raise ConfigurationError, 'Model is required' unless config.model
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def auth_header
|
|
30
|
+
{ 'Authorization' => "Bearer #{config.api_key}" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'openai'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
module LLMClients
|
|
7
|
+
# OpenAI's Responses API. Same host and auth as Chat Completions, but a
|
|
8
|
+
# different request and response shape:
|
|
9
|
+
#
|
|
10
|
+
# - messages go under :input, and the system prompt under :instructions
|
|
11
|
+
# - :max_tokens becomes :max_output_tokens
|
|
12
|
+
# - the reply is an :output array of typed items (message, function_call,
|
|
13
|
+
# reasoning, ...) rather than a single choice
|
|
14
|
+
# - usage is input_tokens / output_tokens
|
|
15
|
+
#
|
|
16
|
+
# Only the non-streaming path is implemented; Responses streams a distinct
|
|
17
|
+
# set of semantic events that the SSE assembly here does not model.
|
|
18
|
+
class OpenAIResponses < OpenAI
|
|
19
|
+
def provider_name
|
|
20
|
+
:openai_responses
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options) # rubocop:disable Lint/UnusedMethodArgument
|
|
24
|
+
payload = {
|
|
25
|
+
model: config.model,
|
|
26
|
+
input: format_input(messages),
|
|
27
|
+
temperature: options[:temperature] || config.temperature,
|
|
28
|
+
max_output_tokens: options[:max_tokens] || config.max_tokens
|
|
29
|
+
}.compact
|
|
30
|
+
|
|
31
|
+
instructions = extract_instructions(messages)
|
|
32
|
+
payload[:instructions] = instructions if instructions
|
|
33
|
+
|
|
34
|
+
if tools && !tools.empty?
|
|
35
|
+
payload[:tools] = format_tools(tools)
|
|
36
|
+
payload[:tool_choice] = tool_choice if tool_choice != :auto
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
plain_chat(payload)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def chat_url
|
|
45
|
+
"#{@base_url}/responses"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Responses carries the system prompt out-of-band as :instructions.
|
|
49
|
+
def extract_instructions(messages)
|
|
50
|
+
systems = messages.select { |m| m.is_a?(Hash) && m[:role].to_s == 'system' }
|
|
51
|
+
return nil if systems.empty?
|
|
52
|
+
|
|
53
|
+
systems.map { |m| m[:content] }.join("\n\n")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_input(messages)
|
|
57
|
+
messages.reject { |m| m.is_a?(Hash) && m[:role].to_s == 'system' }
|
|
58
|
+
.map { |m| { role: m[:role].to_s, content: m[:content] } }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Responses flattens the function definition instead of nesting it under
|
|
62
|
+
# a :function key the way Chat Completions does.
|
|
63
|
+
def format_tools(tools)
|
|
64
|
+
tools.map do |t|
|
|
65
|
+
{ type: 'function', name: t[:name], description: t[:description],
|
|
66
|
+
parameters: t[:parameters] }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def normalize_non_streaming(body)
|
|
71
|
+
output = Array(body['output'])
|
|
72
|
+
text = extract_text(output)
|
|
73
|
+
tool_calls = extract_tool_calls(output)
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
content: text.empty? ? nil : text,
|
|
77
|
+
tool_calls: tool_calls,
|
|
78
|
+
usage: {
|
|
79
|
+
prompt_tokens: body.dig('usage', 'input_tokens'),
|
|
80
|
+
completion_tokens: body.dig('usage', 'output_tokens'),
|
|
81
|
+
total_tokens: body.dig('usage', 'total_tokens')
|
|
82
|
+
},
|
|
83
|
+
finish_reason: finish_reason_for(body, tool_calls),
|
|
84
|
+
model: body['model'] || config.model,
|
|
85
|
+
provider: provider_name
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extract_text(output)
|
|
90
|
+
output.select { |item| item['type'] == 'message' }
|
|
91
|
+
.flat_map { |item| Array(item['content']) }
|
|
92
|
+
.select { |part| part['type'] == 'output_text' }
|
|
93
|
+
.map { |part| part['text'] }
|
|
94
|
+
.join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_tool_calls(output)
|
|
98
|
+
output.select { |item| item['type'] == 'function_call' }.map do |item|
|
|
99
|
+
{
|
|
100
|
+
id: item['call_id'] || item['id'],
|
|
101
|
+
name: item['name'],
|
|
102
|
+
arguments: parse_arguments(item['arguments'])
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_arguments(raw)
|
|
108
|
+
return {} if raw.nil? || raw.empty?
|
|
109
|
+
|
|
110
|
+
JSON.parse(raw)
|
|
111
|
+
rescue JSON::ParserError
|
|
112
|
+
{}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def finish_reason_for(body, tool_calls)
|
|
116
|
+
return :tool_calls if tool_calls.any?
|
|
117
|
+
|
|
118
|
+
if body['status'] == 'incomplete'
|
|
119
|
+
reason = body.dig('incomplete_details', 'reason')
|
|
120
|
+
return reason == 'max_output_tokens' ? :length : :incomplete
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
:stop
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'openai'
|
|
4
|
+
|
|
5
|
+
module RCrewAI
|
|
6
|
+
module LLMClients
|
|
7
|
+
# Snowflake Cortex inference. The request/response bodies follow the
|
|
8
|
+
# OpenAI Chat Completions shape, so only the endpoint and the auth headers
|
|
9
|
+
# differ -- Cortex wants the token type declared alongside the bearer
|
|
10
|
+
# token.
|
|
11
|
+
class SnowflakeCortex < OpenAI
|
|
12
|
+
def initialize(config = RCrewAI.configuration, **hooks)
|
|
13
|
+
super
|
|
14
|
+
@account = config.snowflake_account
|
|
15
|
+
@base_url = "https://#{@account}.snowflakecomputing.com"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def provider_name
|
|
19
|
+
:snowflake
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def chat_url
|
|
25
|
+
"#{@base_url}/api/v2/cortex/inference:complete"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def auth_header
|
|
29
|
+
{
|
|
30
|
+
'Authorization' => "Bearer #{config.api_key}",
|
|
31
|
+
'X-Snowflake-Authorization-Token-Type' => 'KEYPAIR_JWT'
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_config!
|
|
36
|
+
raise ConfigurationError, 'Snowflake token is required' unless config.api_key
|
|
37
|
+
raise ConfigurationError, 'A Snowflake account identifier is required' unless config.snowflake_account
|
|
38
|
+
raise ConfigurationError, 'Model is required' unless config.model
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/rcrewai/process.rb
CHANGED
|
@@ -17,6 +17,18 @@ module RCrewAI
|
|
|
17
17
|
|
|
18
18
|
protected
|
|
19
19
|
|
|
20
|
+
# True when this task's result was replayed from a checkpoint and so must
|
|
21
|
+
# not be executed again.
|
|
22
|
+
def restored?(task)
|
|
23
|
+
crew.respond_to?(:restored_task_names) &&
|
|
24
|
+
crew.restored_task_names.include?(task.name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Records a settled task in the crew's checkpoint, if one is active.
|
|
28
|
+
def checkpoint(task, status)
|
|
29
|
+
crew.checkpoint_task(task, status) if crew.respond_to?(:checkpoint_task)
|
|
30
|
+
end
|
|
31
|
+
|
|
20
32
|
def log_execution_start
|
|
21
33
|
@logger.info "Starting #{self.class.name.split('::').last.downcase} process execution"
|
|
22
34
|
@logger.info "Crew: #{crew.name} with #{crew.agents.length} agents, #{crew.tasks.length} tasks"
|
|
@@ -34,13 +46,21 @@ module RCrewAI
|
|
|
34
46
|
results = []
|
|
35
47
|
|
|
36
48
|
crew.tasks.each do |task|
|
|
49
|
+
if restored?(task)
|
|
50
|
+
@logger.info "Skipping task (restored from checkpoint): #{task.name}"
|
|
51
|
+
results << { task: task, result: task.result, status: :completed }
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
@logger.info "Executing task: #{task.name}"
|
|
38
56
|
begin
|
|
39
57
|
result = task.execute
|
|
40
58
|
results << { task: task, result: result, status: :completed }
|
|
59
|
+
checkpoint(task, :completed)
|
|
41
60
|
rescue StandardError => e
|
|
42
61
|
@logger.error "Task #{task.name} failed: #{e.message}"
|
|
43
62
|
results << { task: task, result: e.message, status: :failed }
|
|
63
|
+
checkpoint(task, :failed)
|
|
44
64
|
end
|
|
45
65
|
end
|
|
46
66
|
|
|
@@ -241,6 +261,15 @@ module RCrewAI
|
|
|
241
261
|
tasks.each do |task|
|
|
242
262
|
assigned_agent = hierarchy[:task_assignments][task]
|
|
243
263
|
|
|
264
|
+
if restored?(task)
|
|
265
|
+
@logger.info "Skipping task (restored from checkpoint): #{task.name}"
|
|
266
|
+
phase_results << {
|
|
267
|
+
task: task, result: task.result, status: :completed,
|
|
268
|
+
assigned_agent: assigned_agent, phase: phase_number
|
|
269
|
+
}
|
|
270
|
+
next
|
|
271
|
+
end
|
|
272
|
+
|
|
244
273
|
@logger.info "Manager delegating '#{task.name}' to #{assigned_agent.name}"
|
|
245
274
|
|
|
246
275
|
begin
|
|
@@ -258,8 +287,10 @@ module RCrewAI
|
|
|
258
287
|
phase: phase_number
|
|
259
288
|
}
|
|
260
289
|
|
|
290
|
+
checkpoint(task, :completed)
|
|
261
291
|
@logger.info "Task '#{task.name}' completed successfully"
|
|
262
292
|
rescue StandardError => e
|
|
293
|
+
checkpoint(task, :failed)
|
|
263
294
|
@logger.error "Delegated task '#{task.name}' failed: #{e.message}"
|
|
264
295
|
|
|
265
296
|
phase_results << {
|
|
@@ -351,7 +382,7 @@ module RCrewAI
|
|
|
351
382
|
end
|
|
352
383
|
|
|
353
384
|
# Execute the task
|
|
354
|
-
agent.execute_task(enhanced_task)
|
|
385
|
+
agent.execute_task(enhanced_task, stream: crew.stream_sink)
|
|
355
386
|
end
|
|
356
387
|
|
|
357
388
|
def should_abort_execution?(failed_tasks, phase_number, _plan)
|
|
@@ -390,11 +421,17 @@ module RCrewAI
|
|
|
390
421
|
private
|
|
391
422
|
|
|
392
423
|
def execute_with_consensus(task)
|
|
424
|
+
if restored?(task)
|
|
425
|
+
@logger.info "Skipping task (restored from checkpoint): #{task.name}"
|
|
426
|
+
return { task: task, result: task.result, status: :completed }
|
|
427
|
+
end
|
|
428
|
+
|
|
393
429
|
@logger.info "Consensus for task: #{task.name}"
|
|
394
430
|
participants = select_participants(task)
|
|
395
431
|
candidates = gather_proposals(task, participants)
|
|
396
432
|
|
|
397
433
|
if candidates.empty?
|
|
434
|
+
checkpoint(task, :failed)
|
|
398
435
|
return { task: task, result: 'All agents failed to produce a proposal', status: :failed }
|
|
399
436
|
end
|
|
400
437
|
|
|
@@ -402,9 +439,11 @@ module RCrewAI
|
|
|
402
439
|
winner = pick_winner(task, scored)
|
|
403
440
|
task.result = winner[:content] if task.respond_to?(:result=)
|
|
404
441
|
|
|
442
|
+
checkpoint(task, :completed)
|
|
405
443
|
{ task: task, result: winner[:content], status: :completed }
|
|
406
444
|
rescue StandardError => e
|
|
407
445
|
@logger.error "Consensus failed for #{task.name}: #{e.message}"
|
|
446
|
+
checkpoint(task, :failed)
|
|
408
447
|
{ task: task, result: e.message, status: :failed }
|
|
409
448
|
end
|
|
410
449
|
|
|
@@ -421,7 +460,7 @@ module RCrewAI
|
|
|
421
460
|
|
|
422
461
|
def gather_proposals(task, participants)
|
|
423
462
|
participants.filter_map do |agent|
|
|
424
|
-
content = extract_content(agent.execute_task(task))
|
|
463
|
+
content = extract_content(agent.execute_task(task, stream: crew.stream_sink))
|
|
425
464
|
{ agent: agent, content: content }
|
|
426
465
|
rescue StandardError => e
|
|
427
466
|
@logger.warn "Agent #{agent.name} failed to propose: #{e.message}"
|
data/lib/rcrewai/task.rb
CHANGED
|
@@ -10,7 +10,7 @@ module RCrewAI
|
|
|
10
10
|
include HumanInteractionExtensions
|
|
11
11
|
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
|
|
12
12
|
:raw_result, :structured_output, :attachments
|
|
13
|
-
attr_accessor :result, :status, :start_time, :end_time, :execution_time
|
|
13
|
+
attr_accessor :result, :status, :start_time, :end_time, :execution_time, :stream_sink
|
|
14
14
|
|
|
15
15
|
def initialize(name:, description:, agent: nil, **options)
|
|
16
16
|
@name = name
|
|
@@ -44,6 +44,7 @@ module RCrewAI
|
|
|
44
44
|
@start_time = nil
|
|
45
45
|
@end_time = nil
|
|
46
46
|
@execution_time = nil
|
|
47
|
+
@stream_sink = nil
|
|
47
48
|
@retry_count = 0
|
|
48
49
|
@max_retries = options.fetch(:max_retries, 2)
|
|
49
50
|
end
|
|
@@ -218,7 +219,7 @@ module RCrewAI
|
|
|
218
219
|
|
|
219
220
|
loop do
|
|
220
221
|
attempts += 1
|
|
221
|
-
raw = extract_content(agent.execute_task(self))
|
|
222
|
+
raw = extract_content(agent.execute_task(self, stream: @stream_sink))
|
|
222
223
|
@raw_result = raw
|
|
223
224
|
|
|
224
225
|
begin
|