rcrewai 0.2.1 → 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 +2 -2
- 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 +39 -39
- metadata +65 -47
data/lib/rcrewai/task.rb
CHANGED
|
@@ -19,13 +19,13 @@ module RCrewAI
|
|
|
19
19
|
@tools = options[:tools] || [] # Additional tools for this specific task
|
|
20
20
|
@async = options[:async] || false # Whether task can run asynchronously
|
|
21
21
|
@callback = options[:callback] # Callback function after completion
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
# Human interaction options
|
|
24
24
|
@human_input_enabled = options[:human_input] || false
|
|
25
25
|
@require_human_confirmation = options[:require_confirmation] || false
|
|
26
26
|
@allow_human_guidance = options[:allow_guidance] || false
|
|
27
27
|
@human_review_points = options[:human_review_points] || []
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
@result = nil
|
|
30
30
|
@status = :pending
|
|
31
31
|
@start_time = nil
|
|
@@ -53,7 +53,7 @@ module RCrewAI
|
|
|
53
53
|
if agent
|
|
54
54
|
validate_dependencies!
|
|
55
55
|
provide_context_to_agent
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
# Enable human input for agent if task allows it
|
|
58
58
|
if @human_input_enabled && agent.respond_to?(:enable_human_input)
|
|
59
59
|
agent.enable_human_input(
|
|
@@ -61,31 +61,28 @@ module RCrewAI
|
|
|
61
61
|
require_approval_for_final_answer: @allow_human_guidance
|
|
62
62
|
)
|
|
63
63
|
end
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
@result = agent.execute_task(self)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
# Post-execution human review if configured
|
|
68
68
|
if @human_input_enabled && @human_review_points.include?(:completion)
|
|
69
69
|
review_result = request_task_completion_review
|
|
70
|
-
if review_result && !review_result[:approved]
|
|
71
|
-
@result = handle_completion_review_feedback(review_result)
|
|
72
|
-
end
|
|
70
|
+
@result = handle_completion_review_feedback(review_result) if review_result && !review_result[:approved]
|
|
73
71
|
end
|
|
74
|
-
|
|
72
|
+
|
|
75
73
|
@status = :completed
|
|
76
74
|
else
|
|
77
|
-
@result =
|
|
75
|
+
@result = 'Task requires an agent'
|
|
78
76
|
@status = :failed
|
|
79
77
|
end
|
|
80
78
|
|
|
81
79
|
@end_time = Time.now
|
|
82
80
|
@execution_time = @end_time - @start_time
|
|
83
|
-
|
|
81
|
+
|
|
84
82
|
# Execute callback if provided
|
|
85
83
|
@callback&.call(self, @result)
|
|
86
|
-
|
|
87
|
-
@result
|
|
88
84
|
|
|
85
|
+
@result
|
|
89
86
|
rescue TaskDependencyError => e
|
|
90
87
|
# Don't retry dependency errors - they need dependencies to be completed first
|
|
91
88
|
@status = :failed
|
|
@@ -93,16 +90,16 @@ module RCrewAI
|
|
|
93
90
|
@execution_time = @end_time - @start_time if @start_time
|
|
94
91
|
@result = "Task dependencies not met: #{e.message}"
|
|
95
92
|
raise e
|
|
96
|
-
rescue => e
|
|
93
|
+
rescue StandardError => e
|
|
97
94
|
@status = :failed
|
|
98
95
|
@end_time = Time.now
|
|
99
96
|
@execution_time = @end_time - @start_time if @start_time
|
|
100
|
-
|
|
97
|
+
|
|
101
98
|
# Retry logic with human intervention
|
|
102
99
|
if @retry_count < @max_retries
|
|
103
100
|
@retry_count += 1
|
|
104
101
|
puts "Task #{name} failed, retrying (#{@retry_count}/#{@max_retries})"
|
|
105
|
-
|
|
102
|
+
|
|
106
103
|
# Ask human for retry guidance if enabled
|
|
107
104
|
if @human_input_enabled
|
|
108
105
|
retry_decision = request_retry_guidance(e)
|
|
@@ -113,15 +110,13 @@ module RCrewAI
|
|
|
113
110
|
when 'modify'
|
|
114
111
|
# Allow human to modify task parameters
|
|
115
112
|
modification_result = request_task_modification
|
|
116
|
-
if modification_result[:modified]
|
|
117
|
-
|
|
118
|
-
end
|
|
119
|
-
# 'retry' is the default - continue with retry logic
|
|
113
|
+
apply_task_modifications(modification_result[:changes]) if modification_result[:modified]
|
|
114
|
+
# 'retry' is the default - continue with retry logic
|
|
120
115
|
end
|
|
121
116
|
end
|
|
122
|
-
|
|
117
|
+
|
|
123
118
|
@status = :pending
|
|
124
|
-
sleep(2
|
|
119
|
+
sleep(2**@retry_count) # Exponential backoff
|
|
125
120
|
return execute
|
|
126
121
|
end
|
|
127
122
|
|
|
@@ -129,14 +124,12 @@ module RCrewAI
|
|
|
129
124
|
raise TaskExecutionError, "Task '#{name}' failed: #{e.message}"
|
|
130
125
|
ensure
|
|
131
126
|
# 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
|
|
127
|
+
agent.disable_human_input if @human_input_enabled && agent.respond_to?(:disable_human_input)
|
|
135
128
|
end
|
|
136
129
|
end
|
|
137
130
|
|
|
138
131
|
def context_data
|
|
139
|
-
return
|
|
132
|
+
return '' if context.empty?
|
|
140
133
|
|
|
141
134
|
context_results = context.map do |task|
|
|
142
135
|
if task.completed?
|
|
@@ -200,13 +193,12 @@ module RCrewAI
|
|
|
200
193
|
def confirm_task_execution
|
|
201
194
|
message = "Confirm execution of task: #{name}"
|
|
202
195
|
context = "Description: #{description}\nExpected Output: #{expected_output || 'Not specified'}\nAssigned Agent: #{agent&.name || 'No agent'}"
|
|
203
|
-
consequences =
|
|
204
|
-
|
|
196
|
+
consequences = 'The task will be executed with the specified agent and may use external tools.'
|
|
197
|
+
|
|
205
198
|
request_human_approval(message,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
199
|
+
context: context,
|
|
200
|
+
consequences: consequences,
|
|
201
|
+
timeout: 60)
|
|
210
202
|
end
|
|
211
203
|
|
|
212
204
|
def request_task_completion_review
|
|
@@ -214,30 +206,30 @@ module RCrewAI
|
|
|
214
206
|
Task: #{name}
|
|
215
207
|
Description: #{description}
|
|
216
208
|
Expected Output: #{expected_output || 'Not specified'}
|
|
217
|
-
|
|
209
|
+
|
|
218
210
|
Actual Result:
|
|
219
211
|
#{result}
|
|
220
|
-
|
|
212
|
+
|
|
221
213
|
Execution Time: #{execution_time&.round(2)} seconds
|
|
222
214
|
Agent: #{agent&.name || 'Unknown'}
|
|
223
215
|
CONTENT
|
|
224
216
|
|
|
225
217
|
request_human_review(
|
|
226
218
|
review_content,
|
|
227
|
-
review_criteria: [
|
|
219
|
+
review_criteria: ['Accuracy', 'Completeness', 'Meets expectations'],
|
|
228
220
|
timeout: 120
|
|
229
221
|
)
|
|
230
222
|
end
|
|
231
223
|
|
|
232
224
|
def handle_completion_review_feedback(review_result)
|
|
233
|
-
if review_result[:suggested_changes]
|
|
225
|
+
if review_result[:suggested_changes]&.any?
|
|
234
226
|
# Ask human how to handle the feedback
|
|
235
227
|
choice_result = request_human_choice(
|
|
236
|
-
|
|
228
|
+
'Task completed but received feedback. How should I proceed?',
|
|
237
229
|
[
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
'Accept result as-is despite feedback',
|
|
231
|
+
'Request agent revision based on feedback',
|
|
232
|
+
'Let me provide the correct result'
|
|
241
233
|
],
|
|
242
234
|
timeout: 60
|
|
243
235
|
)
|
|
@@ -247,13 +239,13 @@ module RCrewAI
|
|
|
247
239
|
result # Return original result
|
|
248
240
|
when 1
|
|
249
241
|
# Request agent to revise (simplified)
|
|
250
|
-
|
|
251
|
-
|
|
242
|
+
"#{result}\n\nNote: Human feedback received: #{review_result[:feedback]}"
|
|
243
|
+
|
|
252
244
|
when 2
|
|
253
245
|
# Get corrected result from human
|
|
254
246
|
correction_result = request_human_input(
|
|
255
|
-
|
|
256
|
-
help_text:
|
|
247
|
+
'Please provide the corrected task result:',
|
|
248
|
+
help_text: 'Enter the complete, corrected result for this task'
|
|
257
249
|
)
|
|
258
250
|
correction_result[:input] || result
|
|
259
251
|
else
|
|
@@ -266,9 +258,9 @@ module RCrewAI
|
|
|
266
258
|
|
|
267
259
|
def request_retry_guidance(error)
|
|
268
260
|
choices = [
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
261
|
+
'Retry with current settings',
|
|
262
|
+
'Modify task parameters and retry',
|
|
263
|
+
'Abort task execution'
|
|
272
264
|
]
|
|
273
265
|
|
|
274
266
|
choice_result = request_human_choice(
|
|
@@ -290,9 +282,9 @@ module RCrewAI
|
|
|
290
282
|
|
|
291
283
|
def request_task_modification
|
|
292
284
|
modification_input = request_human_input(
|
|
293
|
-
|
|
285
|
+
'Please specify task modifications (JSON format):',
|
|
294
286
|
type: :json,
|
|
295
|
-
help_text:
|
|
287
|
+
help_text: 'Provide modifications as JSON, e.g. {"description": "new description", "expected_output": "new output"}'
|
|
296
288
|
)
|
|
297
289
|
|
|
298
290
|
if modification_input[:valid]
|
|
@@ -316,38 +308,38 @@ module RCrewAI
|
|
|
316
308
|
when 'expected_output'
|
|
317
309
|
@expected_output = value
|
|
318
310
|
when 'max_retries'
|
|
319
|
-
@max_retries = value.to_i if value.to_i
|
|
311
|
+
@max_retries = value.to_i if value.to_i.positive?
|
|
320
312
|
end
|
|
321
313
|
end
|
|
322
314
|
end
|
|
323
315
|
|
|
324
316
|
def validate_dependencies!
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
317
|
+
return if dependencies_met?
|
|
318
|
+
|
|
319
|
+
incomplete_deps = context.reject(&:completed?).map(&:name)
|
|
320
|
+
raise TaskDependencyError, "Dependencies not met: #{incomplete_deps.join(', ')}"
|
|
329
321
|
end
|
|
330
322
|
|
|
331
323
|
def provide_context_to_agent
|
|
332
324
|
# Temporarily add task-specific tools to agent
|
|
333
325
|
original_tools = agent.tools.dup
|
|
334
326
|
combined_tools = (agent.tools + @tools).uniq
|
|
335
|
-
|
|
327
|
+
|
|
336
328
|
# Use metaprogramming to temporarily extend agent's tools
|
|
337
329
|
agent.instance_variable_set(:@tools, combined_tools)
|
|
338
|
-
|
|
330
|
+
|
|
339
331
|
# Restore original tools after execution (handled by ensure in execute method)
|
|
340
332
|
at_exit { agent.instance_variable_set(:@tools, original_tools) }
|
|
341
333
|
end
|
|
342
334
|
|
|
343
335
|
class CLI < Thor
|
|
344
|
-
desc
|
|
336
|
+
desc 'new NAME', 'Create a new task'
|
|
345
337
|
option :description, type: :string, required: true
|
|
346
338
|
option :agent, type: :string
|
|
347
339
|
option :expected_output, type: :string
|
|
348
340
|
option :async, type: :boolean, default: false
|
|
349
341
|
def new(name)
|
|
350
|
-
|
|
342
|
+
Task.new(
|
|
351
343
|
name: name,
|
|
352
344
|
description: options[:description],
|
|
353
345
|
agent: options[:agent],
|
|
@@ -361,16 +353,16 @@ module RCrewAI
|
|
|
361
353
|
puts "Async: #{options[:async]}"
|
|
362
354
|
end
|
|
363
355
|
|
|
364
|
-
desc
|
|
356
|
+
desc 'list', 'List all tasks'
|
|
365
357
|
def list
|
|
366
|
-
puts
|
|
367
|
-
puts
|
|
368
|
-
puts
|
|
369
|
-
puts
|
|
358
|
+
puts 'Available tasks:'
|
|
359
|
+
puts ' - research_topic (Agent: researcher)'
|
|
360
|
+
puts ' - write_article (Agent: writer)'
|
|
361
|
+
puts ' - analyze_data (Agent: analyst)'
|
|
370
362
|
end
|
|
371
363
|
end
|
|
372
364
|
end
|
|
373
365
|
|
|
374
366
|
class TaskExecutionError < Error; end
|
|
375
367
|
class TaskDependencyError < TaskExecutionError; end
|
|
376
|
-
end
|
|
368
|
+
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
|