rcrewai 0.1.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 +7 -0
- data/CHANGELOG.md +108 -0
- data/LICENSE +21 -0
- data/README.md +328 -0
- data/Rakefile +130 -0
- data/bin/rcrewai +7 -0
- data/docs/_config.yml +59 -0
- data/docs/_layouts/api.html +16 -0
- data/docs/_layouts/default.html +78 -0
- data/docs/_layouts/example.html +24 -0
- data/docs/_layouts/tutorial.html +33 -0
- data/docs/api/configuration.md +327 -0
- data/docs/api/crew.md +345 -0
- data/docs/api/index.md +41 -0
- data/docs/api/tools.md +412 -0
- data/docs/assets/css/style.css +416 -0
- data/docs/examples/human-in-the-loop.md +382 -0
- data/docs/examples/index.md +78 -0
- data/docs/examples/production-ready-crew.md +485 -0
- data/docs/examples/simple-research-crew.md +297 -0
- data/docs/index.md +353 -0
- data/docs/tutorials/getting-started.md +341 -0
- data/examples/async_execution_example.rb +294 -0
- data/examples/hierarchical_crew_example.rb +193 -0
- data/examples/human_in_the_loop_example.rb +233 -0
- data/lib/rcrewai/agent.rb +636 -0
- data/lib/rcrewai/async_executor.rb +248 -0
- data/lib/rcrewai/cli.rb +39 -0
- data/lib/rcrewai/configuration.rb +100 -0
- data/lib/rcrewai/crew.rb +292 -0
- data/lib/rcrewai/human_input.rb +520 -0
- data/lib/rcrewai/llm_client.rb +41 -0
- data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
- data/lib/rcrewai/llm_clients/azure.rb +158 -0
- data/lib/rcrewai/llm_clients/base.rb +82 -0
- data/lib/rcrewai/llm_clients/google.rb +158 -0
- data/lib/rcrewai/llm_clients/ollama.rb +199 -0
- data/lib/rcrewai/llm_clients/openai.rb +124 -0
- data/lib/rcrewai/memory.rb +194 -0
- data/lib/rcrewai/process.rb +421 -0
- data/lib/rcrewai/task.rb +376 -0
- data/lib/rcrewai/tools/base.rb +82 -0
- data/lib/rcrewai/tools/code_executor.rb +333 -0
- data/lib/rcrewai/tools/email_sender.rb +210 -0
- data/lib/rcrewai/tools/file_reader.rb +111 -0
- data/lib/rcrewai/tools/file_writer.rb +115 -0
- data/lib/rcrewai/tools/pdf_processor.rb +342 -0
- data/lib/rcrewai/tools/sql_database.rb +226 -0
- data/lib/rcrewai/tools/web_search.rb +131 -0
- data/lib/rcrewai/version.rb +5 -0
- data/lib/rcrewai.rb +36 -0
- data/rcrewai.gemspec +54 -0
- metadata +365 -0
@@ -0,0 +1,636 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require_relative 'llm_client'
|
5
|
+
require_relative 'memory'
|
6
|
+
require_relative 'tools/base'
|
7
|
+
require_relative 'human_input'
|
8
|
+
|
9
|
+
module RCrewAI
|
10
|
+
class Agent
|
11
|
+
include HumanInteractionExtensions
|
12
|
+
attr_reader :name, :role, :goal, :backstory, :tools, :memory, :llm_client
|
13
|
+
attr_accessor :verbose, :allow_delegation, :max_iterations, :max_execution_time, :manager
|
14
|
+
|
15
|
+
def initialize(name:, role:, goal:, backstory: nil, tools: [], **options)
|
16
|
+
@name = name
|
17
|
+
@role = role
|
18
|
+
@goal = goal
|
19
|
+
@backstory = backstory
|
20
|
+
@tools = tools
|
21
|
+
@verbose = options.fetch(:verbose, false)
|
22
|
+
@allow_delegation = options.fetch(:allow_delegation, false)
|
23
|
+
@manager = options.fetch(:manager, false) # New manager flag
|
24
|
+
@max_iterations = options.fetch(:max_iterations, 10)
|
25
|
+
@max_execution_time = options.fetch(:max_execution_time, 300) # 5 minutes
|
26
|
+
@human_input_enabled = options.fetch(:human_input, false)
|
27
|
+
@require_approval_for_tools = options.fetch(:require_approval_for_tools, false)
|
28
|
+
@require_approval_for_final_answer = options.fetch(:require_approval_for_final_answer, false)
|
29
|
+
@logger = Logger.new($stdout)
|
30
|
+
@logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
31
|
+
@memory = Memory.new
|
32
|
+
@llm_client = LLMClient.for_provider
|
33
|
+
@subordinates = [] # For manager agents
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute_task(task)
|
37
|
+
@logger.info "Agent #{name} starting task: #{task.name}"
|
38
|
+
start_time = Time.now
|
39
|
+
|
40
|
+
begin
|
41
|
+
# Build context for the agent
|
42
|
+
context = build_context(task)
|
43
|
+
|
44
|
+
# Execute task with reasoning loop
|
45
|
+
result = reasoning_loop(task, context)
|
46
|
+
|
47
|
+
execution_time = Time.now - start_time
|
48
|
+
@logger.info "Task completed in #{execution_time.round(2)}s"
|
49
|
+
|
50
|
+
# Store in memory
|
51
|
+
memory.add_execution(task, result, execution_time)
|
52
|
+
|
53
|
+
task.result = result
|
54
|
+
result
|
55
|
+
|
56
|
+
rescue => e
|
57
|
+
@logger.error "Task execution failed: #{e.message}"
|
58
|
+
task.result = "Task failed: #{e.message}"
|
59
|
+
raise AgentError, "Agent #{name} failed to execute task: #{e.message}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def available_tools_description
|
64
|
+
return "No tools available." if tools.empty?
|
65
|
+
|
66
|
+
tools.map do |tool|
|
67
|
+
"- #{tool.name}: #{tool.description}"
|
68
|
+
end.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
def use_tool(tool_name, **params)
|
72
|
+
tool = tools.find { |t| t.name == tool_name || t.class.name.split('::').last.downcase == tool_name.downcase }
|
73
|
+
raise ToolNotFoundError, "Tool '#{tool_name}' not found" unless tool
|
74
|
+
|
75
|
+
# Request human approval for tool usage if required
|
76
|
+
if @require_approval_for_tools && @human_input_enabled
|
77
|
+
approval_result = request_tool_approval(tool_name, params)
|
78
|
+
unless approval_result[:approved]
|
79
|
+
@logger.info "Tool usage rejected by human: #{tool_name}"
|
80
|
+
return "Tool usage was rejected by human reviewer: #{approval_result[:reason]}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@logger.debug "Using tool: #{tool_name} with params: #{params}"
|
85
|
+
|
86
|
+
begin
|
87
|
+
result = tool.execute(**params)
|
88
|
+
|
89
|
+
# Store tool usage in memory
|
90
|
+
memory.add_tool_usage(tool_name, params, result)
|
91
|
+
|
92
|
+
result
|
93
|
+
rescue => e
|
94
|
+
@logger.error "Tool execution failed: #{e.message}"
|
95
|
+
|
96
|
+
# Offer human intervention if tool fails and human input is enabled
|
97
|
+
if @human_input_enabled
|
98
|
+
handle_tool_failure(tool_name, params, e)
|
99
|
+
else
|
100
|
+
raise e
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Manager-specific methods
|
106
|
+
def is_manager?
|
107
|
+
@manager
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_subordinate(agent)
|
111
|
+
return unless is_manager?
|
112
|
+
@subordinates << agent unless @subordinates.include?(agent)
|
113
|
+
end
|
114
|
+
|
115
|
+
def subordinates
|
116
|
+
@subordinates
|
117
|
+
end
|
118
|
+
|
119
|
+
def delegate_task(task, target_agent)
|
120
|
+
return unless is_manager?
|
121
|
+
return unless @subordinates.include?(target_agent) || allow_delegation
|
122
|
+
|
123
|
+
@logger.info "Manager #{name} delegating task '#{task.name}' to #{target_agent.name}"
|
124
|
+
|
125
|
+
# Create delegation context
|
126
|
+
delegation_prompt = build_delegation_prompt(task, target_agent)
|
127
|
+
|
128
|
+
# Use LLM to create proper delegation
|
129
|
+
response = llm_client.chat(
|
130
|
+
messages: [{ role: 'user', content: delegation_prompt }],
|
131
|
+
temperature: 0.2,
|
132
|
+
max_tokens: 1000
|
133
|
+
)
|
134
|
+
|
135
|
+
delegation_instructions = response[:content]
|
136
|
+
@logger.debug "Delegation instructions: #{delegation_instructions}"
|
137
|
+
|
138
|
+
# Execute delegated task
|
139
|
+
target_agent.execute_delegated_task(task, delegation_instructions, self)
|
140
|
+
end
|
141
|
+
|
142
|
+
def execute_delegated_task(task, delegation_instructions, manager_agent)
|
143
|
+
@logger.info "Receiving delegation from manager #{manager_agent.name}"
|
144
|
+
@logger.debug "Delegation instructions: #{delegation_instructions}"
|
145
|
+
|
146
|
+
# Store delegation context in task
|
147
|
+
original_description = task.description
|
148
|
+
enhanced_description = "#{original_description}\n\nDelegation Instructions from #{manager_agent.name}:\n#{delegation_instructions}"
|
149
|
+
|
150
|
+
# Temporarily modify task
|
151
|
+
task.instance_variable_set(:@description, enhanced_description)
|
152
|
+
task.instance_variable_set(:@manager, manager_agent)
|
153
|
+
|
154
|
+
begin
|
155
|
+
result = execute_task(task)
|
156
|
+
|
157
|
+
# Report back to manager
|
158
|
+
report_to_manager(task, result, manager_agent)
|
159
|
+
|
160
|
+
result
|
161
|
+
ensure
|
162
|
+
# Restore original task description
|
163
|
+
task.instance_variable_set(:@description, original_description)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Human input methods (public)
|
168
|
+
def enable_human_input(**options)
|
169
|
+
@human_input_enabled = true
|
170
|
+
@require_approval_for_tools = options.fetch(:require_approval_for_tools, false)
|
171
|
+
@require_approval_for_final_answer = options.fetch(:require_approval_for_final_answer, false)
|
172
|
+
@logger.info "Human input enabled for agent #{name}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def disable_human_input
|
176
|
+
@human_input_enabled = false
|
177
|
+
@require_approval_for_tools = false
|
178
|
+
@require_approval_for_final_answer = false
|
179
|
+
@logger.info "Human input disabled for agent #{name}"
|
180
|
+
end
|
181
|
+
|
182
|
+
def human_input_enabled?
|
183
|
+
@human_input_enabled
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def build_context(task)
|
189
|
+
context = {
|
190
|
+
agent_role: role,
|
191
|
+
agent_goal: goal,
|
192
|
+
agent_backstory: backstory,
|
193
|
+
task_description: task.description,
|
194
|
+
task_expected_output: task.expected_output,
|
195
|
+
available_tools: available_tools_description,
|
196
|
+
previous_executions: memory.relevant_executions(task),
|
197
|
+
context_data: task.context_data
|
198
|
+
}
|
199
|
+
|
200
|
+
# Add delegation capabilities if allowed
|
201
|
+
if allow_delegation
|
202
|
+
context[:delegation_note] = "You can delegate subtasks to other agents if needed."
|
203
|
+
end
|
204
|
+
|
205
|
+
context
|
206
|
+
end
|
207
|
+
|
208
|
+
def reasoning_loop(task, context)
|
209
|
+
iteration = 0
|
210
|
+
start_time = Time.now
|
211
|
+
|
212
|
+
loop do
|
213
|
+
iteration += 1
|
214
|
+
current_time = Time.now
|
215
|
+
|
216
|
+
# Check limits
|
217
|
+
if iteration > max_iterations
|
218
|
+
@logger.warn "Max iterations (#{max_iterations}) reached"
|
219
|
+
break
|
220
|
+
end
|
221
|
+
|
222
|
+
if current_time - start_time > max_execution_time
|
223
|
+
@logger.warn "Max execution time (#{max_execution_time}s) reached"
|
224
|
+
break
|
225
|
+
end
|
226
|
+
|
227
|
+
# Human review of reasoning at key points
|
228
|
+
if @human_input_enabled && (iteration == 1 || iteration % 3 == 0)
|
229
|
+
review_result = request_reasoning_review(task, context, iteration)
|
230
|
+
if review_result && review_result[:feedback]
|
231
|
+
context[:human_guidance] = review_result[:feedback]
|
232
|
+
@logger.info "Incorporating human guidance into reasoning"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Generate reasoning prompt
|
237
|
+
prompt = build_reasoning_prompt(context, iteration)
|
238
|
+
|
239
|
+
# Get LLM response
|
240
|
+
@logger.debug "Iteration #{iteration}: Sending prompt to LLM"
|
241
|
+
response = llm_client.chat(
|
242
|
+
messages: [{ role: 'user', content: prompt }],
|
243
|
+
temperature: 0.1,
|
244
|
+
max_tokens: 2000
|
245
|
+
)
|
246
|
+
|
247
|
+
reasoning = response[:content]
|
248
|
+
@logger.debug "LLM Response: #{reasoning[0..200]}..." if verbose
|
249
|
+
|
250
|
+
# Parse and execute actions
|
251
|
+
action_result = parse_and_execute_actions(reasoning, task)
|
252
|
+
|
253
|
+
# Check if task is complete
|
254
|
+
if task_complete?(reasoning, action_result)
|
255
|
+
final_result = extract_final_result(reasoning, action_result)
|
256
|
+
|
257
|
+
# Human approval of final result if required
|
258
|
+
if @require_approval_for_final_answer && @human_input_enabled
|
259
|
+
final_result = request_final_answer_approval(final_result)
|
260
|
+
end
|
261
|
+
|
262
|
+
@logger.info "Task completed successfully in #{iteration} iterations"
|
263
|
+
return final_result
|
264
|
+
end
|
265
|
+
|
266
|
+
# Update context with new information
|
267
|
+
context[:previous_reasoning] = reasoning
|
268
|
+
context[:previous_result] = action_result
|
269
|
+
context[:iteration] = iteration
|
270
|
+
end
|
271
|
+
|
272
|
+
# If we exit the loop without completion, return best effort result
|
273
|
+
final_result = extract_final_result(context[:previous_reasoning], context[:previous_result]) ||
|
274
|
+
"Task execution reached limits without clear completion"
|
275
|
+
|
276
|
+
# Human approval even for incomplete results if required
|
277
|
+
if @require_approval_for_final_answer && @human_input_enabled
|
278
|
+
final_result = request_final_answer_approval(final_result)
|
279
|
+
end
|
280
|
+
|
281
|
+
final_result
|
282
|
+
end
|
283
|
+
|
284
|
+
def build_reasoning_prompt(context, iteration)
|
285
|
+
prompt = <<~PROMPT
|
286
|
+
You are #{context[:agent_role]}.
|
287
|
+
|
288
|
+
Your goal: #{context[:agent_goal]}
|
289
|
+
|
290
|
+
Background: #{context[:agent_backstory]}
|
291
|
+
|
292
|
+
Current Task: #{context[:task_description]}
|
293
|
+
Expected Output: #{context[:task_expected_output]}
|
294
|
+
|
295
|
+
Available Tools:
|
296
|
+
#{context[:available_tools]}
|
297
|
+
|
298
|
+
#{context[:delegation_note] if context[:delegation_note]}
|
299
|
+
|
300
|
+
#{build_context_section(context)}
|
301
|
+
|
302
|
+
This is iteration #{iteration}. Think step by step about how to approach this task.
|
303
|
+
|
304
|
+
You can:
|
305
|
+
1. Use tools by writing: USE_TOOL[tool_name](param1=value1, param2=value2)
|
306
|
+
2. Provide your final answer when ready: FINAL_ANSWER[your complete response]
|
307
|
+
3. Continue reasoning if you need more information
|
308
|
+
|
309
|
+
What is your next step?
|
310
|
+
PROMPT
|
311
|
+
|
312
|
+
prompt
|
313
|
+
end
|
314
|
+
|
315
|
+
def build_context_section(context)
|
316
|
+
sections = []
|
317
|
+
|
318
|
+
if context[:context_data] && !context[:context_data].empty?
|
319
|
+
sections << "Additional Context:\n#{context[:context_data]}"
|
320
|
+
end
|
321
|
+
|
322
|
+
if context[:previous_executions] && !context[:previous_executions].empty?
|
323
|
+
sections << "Previous Similar Tasks:\n#{context[:previous_executions]}"
|
324
|
+
end
|
325
|
+
|
326
|
+
if context[:human_guidance]
|
327
|
+
sections << "Human Guidance:\n#{context[:human_guidance]}"
|
328
|
+
end
|
329
|
+
|
330
|
+
if context[:previous_reasoning]
|
331
|
+
sections << "Previous Reasoning:\n#{context[:previous_reasoning]}"
|
332
|
+
end
|
333
|
+
|
334
|
+
if context[:previous_result]
|
335
|
+
sections << "Previous Action Result:\n#{context[:previous_result]}"
|
336
|
+
end
|
337
|
+
|
338
|
+
sections.join("\n\n")
|
339
|
+
end
|
340
|
+
|
341
|
+
def parse_and_execute_actions(reasoning, task)
|
342
|
+
results = []
|
343
|
+
|
344
|
+
# Look for tool usage patterns
|
345
|
+
tool_matches = reasoning.scan(/USE_TOOL\[(\w+)\]\(([^)]*)\)/)
|
346
|
+
tool_matches.each do |tool_name, params_str|
|
347
|
+
begin
|
348
|
+
params = parse_tool_params(params_str)
|
349
|
+
result = use_tool(tool_name, **params)
|
350
|
+
results << "Tool #{tool_name} result: #{result}"
|
351
|
+
rescue => e
|
352
|
+
results << "Tool #{tool_name} failed: #{e.message}"
|
353
|
+
@logger.error "Tool execution failed: #{e.message}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
results.join("\n")
|
358
|
+
end
|
359
|
+
|
360
|
+
def parse_tool_params(params_str)
|
361
|
+
params = {}
|
362
|
+
return params if params_str.strip.empty?
|
363
|
+
|
364
|
+
param_pairs = params_str.split(',').map(&:strip)
|
365
|
+
param_pairs.each do |pair|
|
366
|
+
key, value = pair.split('=', 2).map(&:strip)
|
367
|
+
if key && value
|
368
|
+
# Remove quotes if present
|
369
|
+
value = value.gsub(/^["']|["']$/, '')
|
370
|
+
params[key.to_sym] = value
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
params
|
375
|
+
end
|
376
|
+
|
377
|
+
def task_complete?(reasoning, action_result)
|
378
|
+
reasoning.include?('FINAL_ANSWER[') ||
|
379
|
+
reasoning.downcase.include?('task complete') ||
|
380
|
+
reasoning.downcase.include?('finished')
|
381
|
+
end
|
382
|
+
|
383
|
+
def extract_final_result(reasoning, action_result)
|
384
|
+
# Look for FINAL_ANSWER pattern
|
385
|
+
if match = reasoning.match(/FINAL_ANSWER\[(.*?)\]$/m)
|
386
|
+
return match[1].strip
|
387
|
+
end
|
388
|
+
|
389
|
+
# Otherwise try to extract meaningful result from reasoning
|
390
|
+
lines = reasoning.split("\n").map(&:strip).reject(&:empty?)
|
391
|
+
final_lines = lines.last(3).join(" ")
|
392
|
+
|
393
|
+
return final_lines if final_lines.length > 20
|
394
|
+
|
395
|
+
# Fallback to action result
|
396
|
+
action_result
|
397
|
+
end
|
398
|
+
|
399
|
+
# Manager-specific private methods
|
400
|
+
def build_delegation_prompt(task, target_agent)
|
401
|
+
<<~PROMPT
|
402
|
+
You are #{name}, a #{role}.
|
403
|
+
|
404
|
+
You need to delegate the following task to #{target_agent.name} (#{target_agent.role}):
|
405
|
+
|
406
|
+
Task: #{task.name}
|
407
|
+
Description: #{task.description}
|
408
|
+
Expected Output: #{task.expected_output || 'Not specified'}
|
409
|
+
|
410
|
+
Target Agent Profile:
|
411
|
+
- Role: #{target_agent.role}
|
412
|
+
- Goal: #{target_agent.goal}
|
413
|
+
- Available Tools: #{target_agent.available_tools_description}
|
414
|
+
|
415
|
+
Create clear, specific delegation instructions that:
|
416
|
+
1. Explain why this agent is the right choice for this task
|
417
|
+
2. Provide any additional context or requirements
|
418
|
+
3. Set clear expectations for deliverables
|
419
|
+
4. Include any coordination notes with other team members
|
420
|
+
|
421
|
+
Keep instructions concise but comprehensive.
|
422
|
+
PROMPT
|
423
|
+
end
|
424
|
+
|
425
|
+
def report_to_manager(task, result, manager_agent)
|
426
|
+
@logger.info "Reporting task completion to manager #{manager_agent.name}"
|
427
|
+
|
428
|
+
# Store the delegation result in manager's memory
|
429
|
+
manager_agent.memory.add_execution(
|
430
|
+
task,
|
431
|
+
"Delegated to #{name}: #{result}",
|
432
|
+
task.execution_time || 0
|
433
|
+
)
|
434
|
+
|
435
|
+
# Could enhance with formal reporting mechanism
|
436
|
+
end
|
437
|
+
|
438
|
+
# Human interaction methods
|
439
|
+
def request_tool_approval(tool_name, params)
|
440
|
+
message = "Agent #{name} wants to use tool '#{tool_name}'"
|
441
|
+
context = "Parameters: #{params.inspect}"
|
442
|
+
consequences = "This will execute the #{tool_name} tool with the specified parameters."
|
443
|
+
|
444
|
+
request_human_approval(message,
|
445
|
+
context: context,
|
446
|
+
consequences: consequences,
|
447
|
+
timeout: 60
|
448
|
+
)
|
449
|
+
end
|
450
|
+
|
451
|
+
def handle_tool_failure(tool_name, params, error)
|
452
|
+
@logger.warn "Requesting human intervention for tool failure"
|
453
|
+
|
454
|
+
choices = [
|
455
|
+
"Retry with same parameters",
|
456
|
+
"Retry with different parameters",
|
457
|
+
"Skip this tool and continue",
|
458
|
+
"Abort task execution"
|
459
|
+
]
|
460
|
+
|
461
|
+
choice_result = request_human_choice(
|
462
|
+
"Tool '#{tool_name}' failed with error: #{error.message}. How should I proceed?",
|
463
|
+
choices,
|
464
|
+
timeout: 120
|
465
|
+
)
|
466
|
+
|
467
|
+
case choice_result[:choice_index]
|
468
|
+
when 0
|
469
|
+
# Retry with same parameters
|
470
|
+
@logger.info "Human requested retry with same parameters"
|
471
|
+
tool = tools.find { |t| t.name == tool_name }
|
472
|
+
tool.execute(**params)
|
473
|
+
when 1
|
474
|
+
# Retry with different parameters
|
475
|
+
new_params_result = request_human_input(
|
476
|
+
"Please provide new parameters for #{tool_name} (JSON format):",
|
477
|
+
type: :json,
|
478
|
+
help_text: "Enter parameters as JSON, e.g. {\"param1\": \"value1\"}"
|
479
|
+
)
|
480
|
+
|
481
|
+
if new_params_result[:valid]
|
482
|
+
@logger.info "Human provided new parameters, retrying tool"
|
483
|
+
tool = tools.find { |t| t.name == tool_name }
|
484
|
+
tool.execute(**new_params_result[:processed_input])
|
485
|
+
else
|
486
|
+
"Invalid parameters provided: #{new_params_result[:reason]}"
|
487
|
+
end
|
488
|
+
when 2
|
489
|
+
# Skip tool
|
490
|
+
@logger.info "Human requested to skip failed tool"
|
491
|
+
"Tool execution skipped by human intervention"
|
492
|
+
else
|
493
|
+
# Abort
|
494
|
+
@logger.error "Human requested task abortion due to tool failure"
|
495
|
+
raise AgentError, "Task aborted by human due to tool failure: #{error.message}"
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
def request_final_answer_approval(proposed_answer)
|
500
|
+
return proposed_answer unless @require_approval_for_final_answer && @human_input_enabled
|
501
|
+
|
502
|
+
review_result = request_human_review(
|
503
|
+
proposed_answer,
|
504
|
+
review_criteria: ["Accuracy", "Completeness", "Clarity", "Relevance"],
|
505
|
+
timeout: 180
|
506
|
+
)
|
507
|
+
|
508
|
+
if review_result[:approved]
|
509
|
+
@logger.info "Final answer approved by human"
|
510
|
+
proposed_answer
|
511
|
+
else
|
512
|
+
@logger.info "Human provided feedback on final answer"
|
513
|
+
|
514
|
+
if review_result[:suggested_changes].any?
|
515
|
+
@logger.info "Suggested changes: #{review_result[:suggested_changes].join('; ')}"
|
516
|
+
|
517
|
+
# Ask human what to do with the feedback
|
518
|
+
choice_result = request_human_choice(
|
519
|
+
"How should I handle your feedback?",
|
520
|
+
[
|
521
|
+
"Revise the answer based on feedback",
|
522
|
+
"Use the answer as-is despite feedback",
|
523
|
+
"Let me provide a completely new answer"
|
524
|
+
]
|
525
|
+
)
|
526
|
+
|
527
|
+
case choice_result[:choice_index]
|
528
|
+
when 0
|
529
|
+
# Revise based on feedback
|
530
|
+
revision_context = "Original answer: #{proposed_answer}\n\nFeedback: #{review_result[:feedback]}"
|
531
|
+
revise_answer_with_feedback(revision_context)
|
532
|
+
when 1
|
533
|
+
# Use as-is
|
534
|
+
proposed_answer
|
535
|
+
when 2
|
536
|
+
# Get new answer from human
|
537
|
+
new_answer_result = request_human_input(
|
538
|
+
"Please provide the final answer:",
|
539
|
+
help_text: "Provide the complete answer for this task"
|
540
|
+
)
|
541
|
+
new_answer_result[:input] || proposed_answer
|
542
|
+
else
|
543
|
+
proposed_answer
|
544
|
+
end
|
545
|
+
else
|
546
|
+
# Generic feedback without specific suggestions
|
547
|
+
revise_answer_with_feedback("Original answer: #{proposed_answer}\n\nFeedback: #{review_result[:feedback]}")
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
def revise_answer_with_feedback(feedback_context)
|
553
|
+
@logger.info "Revising answer based on human feedback"
|
554
|
+
|
555
|
+
revision_prompt = <<~PROMPT
|
556
|
+
You are #{name}, a #{role}.
|
557
|
+
|
558
|
+
You need to revise your previous answer based on human feedback.
|
559
|
+
|
560
|
+
#{feedback_context}
|
561
|
+
|
562
|
+
Please provide a revised answer that addresses the feedback while maintaining accuracy and completeness.
|
563
|
+
|
564
|
+
Revised answer:
|
565
|
+
PROMPT
|
566
|
+
|
567
|
+
response = llm_client.chat(
|
568
|
+
messages: [{ role: 'user', content: revision_prompt }],
|
569
|
+
temperature: 0.2,
|
570
|
+
max_tokens: 1000
|
571
|
+
)
|
572
|
+
|
573
|
+
revised_answer = response[:content]
|
574
|
+
@logger.debug "Revised answer based on feedback: #{revised_answer[0..100]}..."
|
575
|
+
|
576
|
+
revised_answer
|
577
|
+
end
|
578
|
+
|
579
|
+
def request_reasoning_review(task, context, iteration)
|
580
|
+
return nil unless @human_input_enabled
|
581
|
+
|
582
|
+
review_content = <<~CONTENT
|
583
|
+
Task: #{task.name}
|
584
|
+
Description: #{task.description}
|
585
|
+
|
586
|
+
Current Iteration: #{iteration}
|
587
|
+
|
588
|
+
Agent Analysis:
|
589
|
+
- Role: #{role}
|
590
|
+
- Current Progress: #{context[:previous_result] || 'Starting task'}
|
591
|
+
- Previous Reasoning: #{context[:previous_reasoning] || 'No previous reasoning'}
|
592
|
+
|
593
|
+
The agent is about to continue reasoning for this task.
|
594
|
+
CONTENT
|
595
|
+
|
596
|
+
request_human_review(
|
597
|
+
review_content,
|
598
|
+
review_criteria: ["Task approach", "Progress assessment", "Strategic guidance"],
|
599
|
+
timeout: 30,
|
600
|
+
optional: true
|
601
|
+
)
|
602
|
+
rescue => e
|
603
|
+
@logger.warn "Failed to get human reasoning review: #{e.message}"
|
604
|
+
nil
|
605
|
+
end
|
606
|
+
|
607
|
+
class CLI < Thor
|
608
|
+
desc "new NAME", "Create a new agent"
|
609
|
+
option :role, type: :string, required: true
|
610
|
+
option :goal, type: :string, required: true
|
611
|
+
option :backstory, type: :string
|
612
|
+
option :verbose, type: :boolean, default: false
|
613
|
+
def new(name)
|
614
|
+
agent = Agent.new(
|
615
|
+
name: name,
|
616
|
+
role: options[:role],
|
617
|
+
goal: options[:goal],
|
618
|
+
backstory: options[:backstory],
|
619
|
+
verbose: options[:verbose]
|
620
|
+
)
|
621
|
+
puts "Agent '#{name}' created with role: #{options[:role]}"
|
622
|
+
end
|
623
|
+
|
624
|
+
desc "list", "List all agents"
|
625
|
+
def list
|
626
|
+
puts "Available agents:"
|
627
|
+
puts " - researcher (Role: Research Specialist)"
|
628
|
+
puts " - writer (Role: Content Writer)"
|
629
|
+
puts " - analyst (Role: Data Analyst)"
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
class AgentError < Error; end
|
635
|
+
class ToolNotFoundError < AgentError; end
|
636
|
+
end
|