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.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +99 -0
- data/CHANGELOG.md +64 -1
- data/README.md +170 -2
- data/ROADMAP.md +84 -0
- 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 +181 -286
- data/lib/rcrewai/async_executor.rb +43 -43
- data/lib/rcrewai/cli.rb +11 -11
- data/lib/rcrewai/configuration.rb +34 -9
- data/lib/rcrewai/crew.rb +134 -39
- data/lib/rcrewai/events.rb +30 -0
- data/lib/rcrewai/flow/state.rb +47 -0
- data/lib/rcrewai/flow/state_store.rb +50 -0
- data/lib/rcrewai/flow.rb +243 -0
- data/lib/rcrewai/human_input.rb +104 -114
- data/lib/rcrewai/knowledge/base.rb +52 -0
- data/lib/rcrewai/knowledge/chunker.rb +31 -0
- data/lib/rcrewai/knowledge/embedder.rb +48 -0
- data/lib/rcrewai/knowledge/sources.rb +83 -0
- data/lib/rcrewai/knowledge/store.rb +58 -0
- data/lib/rcrewai/knowledge.rb +13 -0
- data/lib/rcrewai/legacy_react_runner.rb +172 -0
- data/lib/rcrewai/llm_client.rb +24 -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/output_schema.rb +79 -0
- data/lib/rcrewai/planning.rb +65 -0
- 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 +145 -66
- 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 +20 -10
- data/rcrewai.gemspec +39 -39
- metadata +77 -47
data/lib/rcrewai/task.rb
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'async_executor'
|
|
4
4
|
require_relative 'human_input'
|
|
5
|
+
require_relative 'output_schema'
|
|
5
6
|
|
|
6
7
|
module RCrewAI
|
|
7
8
|
class Task
|
|
8
9
|
include AsyncExtensions
|
|
9
10
|
include HumanInteractionExtensions
|
|
10
|
-
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async
|
|
11
|
+
attr_reader :name, :description, :agent, :context, :expected_output, :tools, :async,
|
|
12
|
+
:raw_result, :structured_output
|
|
11
13
|
attr_accessor :result, :status, :start_time, :end_time, :execution_time
|
|
12
14
|
|
|
13
15
|
def initialize(name:, description:, agent: nil, **options)
|
|
@@ -19,13 +21,23 @@ module RCrewAI
|
|
|
19
21
|
@tools = options[:tools] || [] # Additional tools for this specific task
|
|
20
22
|
@async = options[:async] || false # Whether task can run asynchronously
|
|
21
23
|
@callback = options[:callback] # Callback function after completion
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
# Output processing (0.4.0)
|
|
26
|
+
@output_schema = options[:output_schema] # JSON-schema for structured output
|
|
27
|
+
@guardrail = options[:guardrail] # ->(output) { [ok, value_or_error] }
|
|
28
|
+
@guardrail_max_retries = options.fetch(:guardrail_max_retries, 3)
|
|
29
|
+
@output_file = options[:output_file] # Path to write result to
|
|
30
|
+
@create_directory = options.fetch(:create_directory, true)
|
|
31
|
+
@markdown = options.fetch(:markdown, false)
|
|
32
|
+
@raw_result = nil # Unprocessed string content
|
|
33
|
+
@structured_output = nil # Parsed object when output_schema set
|
|
34
|
+
|
|
23
35
|
# Human interaction options
|
|
24
36
|
@human_input_enabled = options[:human_input] || false
|
|
25
37
|
@require_human_confirmation = options[:require_confirmation] || false
|
|
26
38
|
@allow_human_guidance = options[:allow_guidance] || false
|
|
27
39
|
@human_review_points = options[:human_review_points] || []
|
|
28
|
-
|
|
40
|
+
|
|
29
41
|
@result = nil
|
|
30
42
|
@status = :pending
|
|
31
43
|
@start_time = nil
|
|
@@ -53,7 +65,7 @@ module RCrewAI
|
|
|
53
65
|
if agent
|
|
54
66
|
validate_dependencies!
|
|
55
67
|
provide_context_to_agent
|
|
56
|
-
|
|
68
|
+
|
|
57
69
|
# Enable human input for agent if task allows it
|
|
58
70
|
if @human_input_enabled && agent.respond_to?(:enable_human_input)
|
|
59
71
|
agent.enable_human_input(
|
|
@@ -61,31 +73,28 @@ module RCrewAI
|
|
|
61
73
|
require_approval_for_final_answer: @allow_human_guidance
|
|
62
74
|
)
|
|
63
75
|
end
|
|
64
|
-
|
|
65
|
-
@result =
|
|
66
|
-
|
|
76
|
+
|
|
77
|
+
@result = run_agent_with_output_processing
|
|
78
|
+
|
|
67
79
|
# Post-execution human review if configured
|
|
68
80
|
if @human_input_enabled && @human_review_points.include?(:completion)
|
|
69
81
|
review_result = request_task_completion_review
|
|
70
|
-
if review_result && !review_result[:approved]
|
|
71
|
-
@result = handle_completion_review_feedback(review_result)
|
|
72
|
-
end
|
|
82
|
+
@result = handle_completion_review_feedback(review_result) if review_result && !review_result[:approved]
|
|
73
83
|
end
|
|
74
|
-
|
|
84
|
+
|
|
75
85
|
@status = :completed
|
|
76
86
|
else
|
|
77
|
-
@result =
|
|
87
|
+
@result = 'Task requires an agent'
|
|
78
88
|
@status = :failed
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
@end_time = Time.now
|
|
82
92
|
@execution_time = @end_time - @start_time
|
|
83
|
-
|
|
93
|
+
|
|
84
94
|
# Execute callback if provided
|
|
85
95
|
@callback&.call(self, @result)
|
|
86
|
-
|
|
87
|
-
@result
|
|
88
96
|
|
|
97
|
+
@result
|
|
89
98
|
rescue TaskDependencyError => e
|
|
90
99
|
# Don't retry dependency errors - they need dependencies to be completed first
|
|
91
100
|
@status = :failed
|
|
@@ -93,16 +102,16 @@ module RCrewAI
|
|
|
93
102
|
@execution_time = @end_time - @start_time if @start_time
|
|
94
103
|
@result = "Task dependencies not met: #{e.message}"
|
|
95
104
|
raise e
|
|
96
|
-
rescue => e
|
|
105
|
+
rescue StandardError => e
|
|
97
106
|
@status = :failed
|
|
98
107
|
@end_time = Time.now
|
|
99
108
|
@execution_time = @end_time - @start_time if @start_time
|
|
100
|
-
|
|
109
|
+
|
|
101
110
|
# Retry logic with human intervention
|
|
102
111
|
if @retry_count < @max_retries
|
|
103
112
|
@retry_count += 1
|
|
104
113
|
puts "Task #{name} failed, retrying (#{@retry_count}/#{@max_retries})"
|
|
105
|
-
|
|
114
|
+
|
|
106
115
|
# Ask human for retry guidance if enabled
|
|
107
116
|
if @human_input_enabled
|
|
108
117
|
retry_decision = request_retry_guidance(e)
|
|
@@ -113,15 +122,13 @@ module RCrewAI
|
|
|
113
122
|
when 'modify'
|
|
114
123
|
# Allow human to modify task parameters
|
|
115
124
|
modification_result = request_task_modification
|
|
116
|
-
if modification_result[:modified]
|
|
117
|
-
|
|
118
|
-
end
|
|
119
|
-
# 'retry' is the default - continue with retry logic
|
|
125
|
+
apply_task_modifications(modification_result[:changes]) if modification_result[:modified]
|
|
126
|
+
# 'retry' is the default - continue with retry logic
|
|
120
127
|
end
|
|
121
128
|
end
|
|
122
|
-
|
|
129
|
+
|
|
123
130
|
@status = :pending
|
|
124
|
-
sleep(2
|
|
131
|
+
sleep(2**@retry_count) # Exponential backoff
|
|
125
132
|
return execute
|
|
126
133
|
end
|
|
127
134
|
|
|
@@ -129,14 +136,12 @@ module RCrewAI
|
|
|
129
136
|
raise TaskExecutionError, "Task '#{name}' failed: #{e.message}"
|
|
130
137
|
ensure
|
|
131
138
|
# Disable human input for agent after task completion
|
|
132
|
-
if @human_input_enabled && agent.respond_to?(:disable_human_input)
|
|
133
|
-
agent.disable_human_input
|
|
134
|
-
end
|
|
139
|
+
agent.disable_human_input if @human_input_enabled && agent.respond_to?(:disable_human_input)
|
|
135
140
|
end
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
def context_data
|
|
139
|
-
return
|
|
144
|
+
return '' if context.empty?
|
|
140
145
|
|
|
141
146
|
context_results = context.map do |task|
|
|
142
147
|
if task.completed?
|
|
@@ -173,6 +178,12 @@ module RCrewAI
|
|
|
173
178
|
@context << task unless @context.include?(task)
|
|
174
179
|
end
|
|
175
180
|
|
|
181
|
+
# Appends supplementary guidance (e.g. a planning step) to the task's
|
|
182
|
+
# description without discarding the original instructions.
|
|
183
|
+
def enrich_description(text)
|
|
184
|
+
@description = "#{@description}\n\n#{text}"
|
|
185
|
+
end
|
|
186
|
+
|
|
176
187
|
def add_tool(tool)
|
|
177
188
|
@tools << tool unless @tools.include?(tool)
|
|
178
189
|
end
|
|
@@ -197,16 +208,83 @@ module RCrewAI
|
|
|
197
208
|
|
|
198
209
|
private
|
|
199
210
|
|
|
211
|
+
# Runs the agent, then applies guardrail validation and schema coercion.
|
|
212
|
+
# Guardrail/schema failures re-run the agent (up to @guardrail_max_retries)
|
|
213
|
+
# with the failure fed back in, rather than raising immediately.
|
|
214
|
+
def run_agent_with_output_processing
|
|
215
|
+
attempts = 0
|
|
216
|
+
feedback = nil
|
|
217
|
+
|
|
218
|
+
loop do
|
|
219
|
+
attempts += 1
|
|
220
|
+
raw = extract_content(agent.execute_task(self))
|
|
221
|
+
@raw_result = raw
|
|
222
|
+
|
|
223
|
+
begin
|
|
224
|
+
apply_guardrail!(raw)
|
|
225
|
+
@structured_output = OutputSchema.coerce(raw, @output_schema) if @output_schema
|
|
226
|
+
rescue OutputProcessingError, OutputSchemaError => e
|
|
227
|
+
if attempts <= @guardrail_max_retries
|
|
228
|
+
feedback = e.message
|
|
229
|
+
append_feedback_to_description(feedback)
|
|
230
|
+
next
|
|
231
|
+
end
|
|
232
|
+
raise
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
write_output_file(guardrail_value_or(raw)) if @output_file
|
|
236
|
+
return guardrail_value_or(raw)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Accepts either the legacy plain-string return or the 0.3.0 result hash.
|
|
241
|
+
def extract_content(agent_result)
|
|
242
|
+
return agent_result[:content].to_s if agent_result.is_a?(Hash)
|
|
243
|
+
|
|
244
|
+
agent_result.to_s
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def apply_guardrail!(raw)
|
|
248
|
+
return unless @guardrail
|
|
249
|
+
|
|
250
|
+
ok, value = @guardrail.call(raw)
|
|
251
|
+
raise OutputProcessingError, "guardrail rejected output: #{value}" unless ok
|
|
252
|
+
|
|
253
|
+
@guardrail_value = value
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def guardrail_value_or(raw)
|
|
257
|
+
@guardrail ? @guardrail_value : raw
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def append_feedback_to_description(feedback)
|
|
261
|
+
@description = "#{@description}\n\n[Retry] Previous attempt was rejected: #{feedback}. " \
|
|
262
|
+
'Please correct the output.'
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def write_output_file(content)
|
|
266
|
+
dir = File.dirname(@output_file)
|
|
267
|
+
if @create_directory
|
|
268
|
+
require 'fileutils'
|
|
269
|
+
FileUtils.mkdir_p(dir)
|
|
270
|
+
elsif !Dir.exist?(dir)
|
|
271
|
+
raise OutputProcessingError, "output directory does not exist: #{dir}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
body = content.to_s
|
|
275
|
+
body = "# #{name}\n\n#{body}" if @markdown && !body.lstrip.start_with?('#')
|
|
276
|
+
File.write(@output_file, body)
|
|
277
|
+
end
|
|
278
|
+
|
|
200
279
|
def confirm_task_execution
|
|
201
280
|
message = "Confirm execution of task: #{name}"
|
|
202
281
|
context = "Description: #{description}\nExpected Output: #{expected_output || 'Not specified'}\nAssigned Agent: #{agent&.name || 'No agent'}"
|
|
203
|
-
consequences =
|
|
204
|
-
|
|
282
|
+
consequences = 'The task will be executed with the specified agent and may use external tools.'
|
|
283
|
+
|
|
205
284
|
request_human_approval(message,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
285
|
+
context: context,
|
|
286
|
+
consequences: consequences,
|
|
287
|
+
timeout: 60)
|
|
210
288
|
end
|
|
211
289
|
|
|
212
290
|
def request_task_completion_review
|
|
@@ -214,30 +292,30 @@ module RCrewAI
|
|
|
214
292
|
Task: #{name}
|
|
215
293
|
Description: #{description}
|
|
216
294
|
Expected Output: #{expected_output || 'Not specified'}
|
|
217
|
-
|
|
295
|
+
|
|
218
296
|
Actual Result:
|
|
219
297
|
#{result}
|
|
220
|
-
|
|
298
|
+
|
|
221
299
|
Execution Time: #{execution_time&.round(2)} seconds
|
|
222
300
|
Agent: #{agent&.name || 'Unknown'}
|
|
223
301
|
CONTENT
|
|
224
302
|
|
|
225
303
|
request_human_review(
|
|
226
304
|
review_content,
|
|
227
|
-
review_criteria: [
|
|
305
|
+
review_criteria: ['Accuracy', 'Completeness', 'Meets expectations'],
|
|
228
306
|
timeout: 120
|
|
229
307
|
)
|
|
230
308
|
end
|
|
231
309
|
|
|
232
310
|
def handle_completion_review_feedback(review_result)
|
|
233
|
-
if review_result[:suggested_changes]
|
|
311
|
+
if review_result[:suggested_changes]&.any?
|
|
234
312
|
# Ask human how to handle the feedback
|
|
235
313
|
choice_result = request_human_choice(
|
|
236
|
-
|
|
314
|
+
'Task completed but received feedback. How should I proceed?',
|
|
237
315
|
[
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
316
|
+
'Accept result as-is despite feedback',
|
|
317
|
+
'Request agent revision based on feedback',
|
|
318
|
+
'Let me provide the correct result'
|
|
241
319
|
],
|
|
242
320
|
timeout: 60
|
|
243
321
|
)
|
|
@@ -247,13 +325,13 @@ module RCrewAI
|
|
|
247
325
|
result # Return original result
|
|
248
326
|
when 1
|
|
249
327
|
# Request agent to revise (simplified)
|
|
250
|
-
|
|
251
|
-
|
|
328
|
+
"#{result}\n\nNote: Human feedback received: #{review_result[:feedback]}"
|
|
329
|
+
|
|
252
330
|
when 2
|
|
253
331
|
# Get corrected result from human
|
|
254
332
|
correction_result = request_human_input(
|
|
255
|
-
|
|
256
|
-
help_text:
|
|
333
|
+
'Please provide the corrected task result:',
|
|
334
|
+
help_text: 'Enter the complete, corrected result for this task'
|
|
257
335
|
)
|
|
258
336
|
correction_result[:input] || result
|
|
259
337
|
else
|
|
@@ -266,9 +344,9 @@ module RCrewAI
|
|
|
266
344
|
|
|
267
345
|
def request_retry_guidance(error)
|
|
268
346
|
choices = [
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
347
|
+
'Retry with current settings',
|
|
348
|
+
'Modify task parameters and retry',
|
|
349
|
+
'Abort task execution'
|
|
272
350
|
]
|
|
273
351
|
|
|
274
352
|
choice_result = request_human_choice(
|
|
@@ -290,9 +368,9 @@ module RCrewAI
|
|
|
290
368
|
|
|
291
369
|
def request_task_modification
|
|
292
370
|
modification_input = request_human_input(
|
|
293
|
-
|
|
371
|
+
'Please specify task modifications (JSON format):',
|
|
294
372
|
type: :json,
|
|
295
|
-
help_text:
|
|
373
|
+
help_text: 'Provide modifications as JSON, e.g. {"description": "new description", "expected_output": "new output"}'
|
|
296
374
|
)
|
|
297
375
|
|
|
298
376
|
if modification_input[:valid]
|
|
@@ -316,38 +394,38 @@ module RCrewAI
|
|
|
316
394
|
when 'expected_output'
|
|
317
395
|
@expected_output = value
|
|
318
396
|
when 'max_retries'
|
|
319
|
-
@max_retries = value.to_i if value.to_i
|
|
397
|
+
@max_retries = value.to_i if value.to_i.positive?
|
|
320
398
|
end
|
|
321
399
|
end
|
|
322
400
|
end
|
|
323
401
|
|
|
324
402
|
def validate_dependencies!
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
403
|
+
return if dependencies_met?
|
|
404
|
+
|
|
405
|
+
incomplete_deps = context.reject(&:completed?).map(&:name)
|
|
406
|
+
raise TaskDependencyError, "Dependencies not met: #{incomplete_deps.join(', ')}"
|
|
329
407
|
end
|
|
330
408
|
|
|
331
409
|
def provide_context_to_agent
|
|
332
410
|
# Temporarily add task-specific tools to agent
|
|
333
411
|
original_tools = agent.tools.dup
|
|
334
412
|
combined_tools = (agent.tools + @tools).uniq
|
|
335
|
-
|
|
413
|
+
|
|
336
414
|
# Use metaprogramming to temporarily extend agent's tools
|
|
337
415
|
agent.instance_variable_set(:@tools, combined_tools)
|
|
338
|
-
|
|
416
|
+
|
|
339
417
|
# Restore original tools after execution (handled by ensure in execute method)
|
|
340
418
|
at_exit { agent.instance_variable_set(:@tools, original_tools) }
|
|
341
419
|
end
|
|
342
420
|
|
|
343
421
|
class CLI < Thor
|
|
344
|
-
desc
|
|
422
|
+
desc 'new NAME', 'Create a new task'
|
|
345
423
|
option :description, type: :string, required: true
|
|
346
424
|
option :agent, type: :string
|
|
347
425
|
option :expected_output, type: :string
|
|
348
426
|
option :async, type: :boolean, default: false
|
|
349
427
|
def new(name)
|
|
350
|
-
|
|
428
|
+
Task.new(
|
|
351
429
|
name: name,
|
|
352
430
|
description: options[:description],
|
|
353
431
|
agent: options[:agent],
|
|
@@ -361,16 +439,17 @@ module RCrewAI
|
|
|
361
439
|
puts "Async: #{options[:async]}"
|
|
362
440
|
end
|
|
363
441
|
|
|
364
|
-
desc
|
|
442
|
+
desc 'list', 'List all tasks'
|
|
365
443
|
def list
|
|
366
|
-
puts
|
|
367
|
-
puts
|
|
368
|
-
puts
|
|
369
|
-
puts
|
|
444
|
+
puts 'Available tasks:'
|
|
445
|
+
puts ' - research_topic (Agent: researcher)'
|
|
446
|
+
puts ' - write_article (Agent: writer)'
|
|
447
|
+
puts ' - analyze_data (Agent: analyst)'
|
|
370
448
|
end
|
|
371
449
|
end
|
|
372
450
|
end
|
|
373
451
|
|
|
374
452
|
class TaskExecutionError < Error; end
|
|
375
453
|
class TaskDependencyError < TaskExecutionError; end
|
|
376
|
-
end
|
|
454
|
+
class OutputProcessingError < TaskExecutionError; end
|
|
455
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'events'
|
|
4
|
+
require_relative 'provider_schema'
|
|
5
|
+
|
|
6
|
+
module RCrewAI
|
|
7
|
+
class ToolRunner
|
|
8
|
+
DEFAULT_MAX_ITERATIONS = 10
|
|
9
|
+
|
|
10
|
+
def initialize(agent:, llm:, tools:, max_iterations: DEFAULT_MAX_ITERATIONS, event_sink: nil)
|
|
11
|
+
@agent = agent
|
|
12
|
+
@llm = llm
|
|
13
|
+
@tools = tools
|
|
14
|
+
@tools_by_name = tools.each_with_object({}) { |t, h| h[t.name] = t }
|
|
15
|
+
@max_iterations = max_iterations
|
|
16
|
+
@sink = event_sink || ->(_) {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(messages:) # rubocop:disable Metrics/AbcSize
|
|
20
|
+
msgs = messages.dup
|
|
21
|
+
history = []
|
|
22
|
+
iter = 0
|
|
23
|
+
total_usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
24
|
+
|
|
25
|
+
while iter < @max_iterations
|
|
26
|
+
iter += 1
|
|
27
|
+
emit(Events::IterationStart, iteration: iter, iteration_index: iter)
|
|
28
|
+
|
|
29
|
+
response = @llm.chat(
|
|
30
|
+
messages: msgs,
|
|
31
|
+
tools: @tools.map(&:json_schema),
|
|
32
|
+
stream: ->(e) { @sink.call(retag(e, iter)) }
|
|
33
|
+
)
|
|
34
|
+
accumulate_usage(total_usage, response[:usage])
|
|
35
|
+
|
|
36
|
+
if response[:tool_calls].nil? || response[:tool_calls].empty?
|
|
37
|
+
emit(Events::IterationEnd, iteration: iter, finish_reason: response[:finish_reason])
|
|
38
|
+
return finalize(content: response[:content], history: history, iter: iter,
|
|
39
|
+
finish_reason: response[:finish_reason], usage: total_usage)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
msgs << { role: 'assistant', content: response[:content], tool_calls: response[:tool_calls] }
|
|
43
|
+
|
|
44
|
+
response[:tool_calls].each do |tc|
|
|
45
|
+
tool = @tools_by_name[tc[:name]]
|
|
46
|
+
emit(Events::ToolCallStart, iteration: iter,
|
|
47
|
+
tool: tc[:name], args: tc[:arguments], call_id: tc[:id])
|
|
48
|
+
|
|
49
|
+
if tool.nil?
|
|
50
|
+
err = "tool not found: #{tc[:name]}"
|
|
51
|
+
emit(Events::ToolCallError, iteration: iter,
|
|
52
|
+
tool: tc[:name], call_id: tc[:id], error: err)
|
|
53
|
+
msgs << tool_result_message(tc[:id], "ERROR: #{err}")
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
started = monotonic_ms
|
|
58
|
+
begin
|
|
59
|
+
result = tool.execute_with_validation(tc[:arguments] || {})
|
|
60
|
+
duration = monotonic_ms - started
|
|
61
|
+
@agent.memory.add_tool_usage(tc[:name], tc[:arguments], result) if @agent.respond_to?(:memory) && @agent.memory
|
|
62
|
+
emit(Events::ToolCallResult, iteration: iter,
|
|
63
|
+
tool: tc[:name], call_id: tc[:id], result: result,
|
|
64
|
+
duration_ms: duration)
|
|
65
|
+
history << { tool: tc[:name], args: tc[:arguments], result: result, duration_ms: duration }
|
|
66
|
+
msgs << tool_result_message(tc[:id], result.to_s)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
emit(Events::ToolCallError, iteration: iter,
|
|
69
|
+
tool: tc[:name], call_id: tc[:id], error: e.message)
|
|
70
|
+
msgs << tool_result_message(tc[:id], "ERROR: #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
emit(Events::IterationEnd, iteration: iter, finish_reason: :tool_calls)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
finalize(content: nil, history: history, iter: iter,
|
|
78
|
+
finish_reason: :max_iterations, usage: total_usage)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def tool_result_message(call_id, content)
|
|
84
|
+
{ role: 'tool', tool_call_id: call_id, content: content }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def emit(klass, iteration:, **attrs)
|
|
88
|
+
type_sym = klass.name.split('::').last
|
|
89
|
+
.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
|
|
90
|
+
.sub(/^_/, '').to_sym
|
|
91
|
+
@sink.call(klass.new(
|
|
92
|
+
type: type_sym,
|
|
93
|
+
timestamp: Time.now,
|
|
94
|
+
agent: agent_name,
|
|
95
|
+
iteration: iteration,
|
|
96
|
+
**attrs
|
|
97
|
+
))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def agent_name
|
|
101
|
+
@agent.respond_to?(:name) ? @agent.name : nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def retag(event, iter)
|
|
105
|
+
event.agent = agent_name if event.respond_to?(:agent=) && event.agent.nil?
|
|
106
|
+
event.iteration = iter if event.respond_to?(:iteration=) && event.iteration.nil?
|
|
107
|
+
event
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def accumulate_usage(total, partial)
|
|
111
|
+
return unless partial.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
total[:prompt_tokens] += partial[:prompt_tokens] || 0
|
|
114
|
+
total[:completion_tokens] += partial[:completion_tokens] || 0
|
|
115
|
+
total[:total_tokens] += partial[:total_tokens] || 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def finalize(content:, history:, iter:, finish_reason:, usage:)
|
|
119
|
+
{
|
|
120
|
+
content: content,
|
|
121
|
+
tool_calls_history: history,
|
|
122
|
+
usage: usage,
|
|
123
|
+
iterations: iter,
|
|
124
|
+
finish_reason: finish_reason
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def monotonic_ms
|
|
129
|
+
(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) * 1000).to_i
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCrewAI
|
|
4
|
+
module ToolSchema
|
|
5
|
+
TYPE_MAP = {
|
|
6
|
+
string: 'string', integer: 'integer', number: 'number',
|
|
7
|
+
boolean: 'boolean', array: 'array', object: 'object', enum: 'string'
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def self.extended(base)
|
|
11
|
+
base.instance_variable_set(:@params, [])
|
|
12
|
+
base.instance_variable_set(:@tool_name, nil)
|
|
13
|
+
base.instance_variable_set(:@description, nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tool_name(name = nil)
|
|
17
|
+
return @tool_name || name_default if name.nil?
|
|
18
|
+
|
|
19
|
+
@tool_name = name.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description(desc = nil)
|
|
23
|
+
return @description || '' if desc.nil?
|
|
24
|
+
|
|
25
|
+
@description = desc.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def param(name, type:, required: false, default: nil, description: nil, # rubocop:disable Metrics/ParameterLists
|
|
29
|
+
items: nil, values: nil, properties: nil)
|
|
30
|
+
@params ||= []
|
|
31
|
+
@params << {
|
|
32
|
+
name: name, type: type, required: required, default: default,
|
|
33
|
+
description: description, items: items, values: values, properties: properties
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def params
|
|
38
|
+
@params || []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def json_schema
|
|
42
|
+
props = {}
|
|
43
|
+
required = []
|
|
44
|
+
params.each do |p|
|
|
45
|
+
entry = { type: TYPE_MAP.fetch(p[:type]) }
|
|
46
|
+
entry[:description] = p[:description] if p[:description]
|
|
47
|
+
entry[:default] = p[:default] unless p[:default].nil?
|
|
48
|
+
entry[:items] = stringify_type(p[:items]) if p[:items]
|
|
49
|
+
entry[:enum] = p[:values] if p[:type] == :enum
|
|
50
|
+
entry[:properties] = p[:properties] if p[:properties]
|
|
51
|
+
props[p[:name]] = entry
|
|
52
|
+
required << p[:name].to_s if p[:required]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if params.empty?
|
|
56
|
+
warn_once_no_dsl!
|
|
57
|
+
return {
|
|
58
|
+
name: tool_name,
|
|
59
|
+
description: description,
|
|
60
|
+
parameters: { type: 'object', additionalProperties: true }
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
name: tool_name,
|
|
66
|
+
description: description,
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: props,
|
|
70
|
+
required: required
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def name_default
|
|
78
|
+
raw = name.to_s.split('::').last
|
|
79
|
+
return '' if raw.nil? || raw.empty?
|
|
80
|
+
|
|
81
|
+
raw.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stringify_type(hash)
|
|
85
|
+
return hash unless hash.is_a?(Hash) && hash[:type].is_a?(Symbol)
|
|
86
|
+
|
|
87
|
+
hash.merge(type: TYPE_MAP.fetch(hash[:type]))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def warn_once_no_dsl!
|
|
91
|
+
return if @warned_no_dsl
|
|
92
|
+
|
|
93
|
+
@warned_no_dsl = true
|
|
94
|
+
Kernel.warn "[rcrewai] Tool #{name} has no DSL declarations; using permissive schema. Declare tool_name/description/param to opt in to a strict JSON schema."
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|