foobara-agent 0.0.6 → 0.0.8
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 +12 -0
- data/lib/foobara/agent.rb +0 -1
- data/src/foobara/agent/accomplish_goal.rb +147 -128
- data/src/foobara/agent/determine_inputs_for_next_command.rb +8 -1
- data/src/foobara/agent/determine_next_command.rb +21 -36
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +4 -1
- data/src/foobara/agent/notify_user_that_current_goal_has_been_accomplished.rb +48 -34
- data/src/foobara/agent.rb +54 -29
- metadata +1 -2
- data/src/foobara/agent/list_types.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12e8facdeb95a000d81ec0ba8a133affa1ea66e470115e978bdc81f05bc51855
|
4
|
+
data.tar.gz: 294064df166d152c037db13263863b8ccfe32f064a78e77f6f5fe59af0be3829
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c7fcaf044ba9bc821dcc42f9b0fb44b412fb42f9ef77dd4b7f3ed13cb8293cdb60d448d136e4e0567d104ecb022a00accfd05dbcf955e38185884d34e59668e
|
7
|
+
data.tar.gz: 380d9ae749b6a942b4e33b112f5aae35fd9600fa9ffcaf462fc0a35122354aae0658378190a4b9db3c0d65d2a12c9d49305b1d03a2881ce504d6d268cf039255
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## [0.0.8] - 2025-06-27
|
2
|
+
|
3
|
+
- Improve what is logged and its formatting when verbose
|
4
|
+
- Improve/experiment with what is stored in the command log
|
5
|
+
- Experiment with Atoms instead of Aggregates
|
6
|
+
- Add max_llm_calls_per_minute option
|
7
|
+
|
8
|
+
## [0.0.7] - 2025-06-23
|
9
|
+
|
10
|
+
- Improvements to result type handling
|
11
|
+
- Eliminate DescribeType command
|
12
|
+
|
1
13
|
## [0.0.6] - 2025-06-19
|
2
14
|
|
3
15
|
- Do not run next command with pre-cast inputs
|
data/lib/foobara/agent.rb
CHANGED
@@ -3,21 +3,22 @@ require_relative "list_commands"
|
|
3
3
|
module Foobara
|
4
4
|
class Agent < CommandConnector
|
5
5
|
class AccomplishGoal < Foobara::Command
|
6
|
+
# Using a const here so we can stub it in the test suite to speed things up
|
7
|
+
SECONDS_PER_MINUTE = 60
|
8
|
+
|
6
9
|
possible_error :gave_up, context: { reason: :string }, message: "Gave up."
|
7
10
|
possible_error :too_many_command_calls,
|
8
11
|
context: { maximum_command_calls: :integer }
|
9
12
|
|
10
13
|
inputs do
|
11
|
-
agent_name :string, "Name of the agent"
|
12
14
|
goal :string, :required, "What do you want the agent to attempt to accomplish?"
|
13
15
|
# TODO: we should be able to specify a subclass as a type
|
14
|
-
command_classes [Class], "Commands that can be ran to accomplish the goal"
|
15
16
|
final_result_type :duck, "Specifies how the result of the goal is to be structured"
|
17
|
+
include_message_to_user_in_result :boolean, default: true
|
16
18
|
verbose :boolean, default: false
|
17
19
|
io_out :duck
|
18
20
|
io_err :duck
|
19
|
-
|
20
|
-
"A connector containing already-connected commands for the agent to use"
|
21
|
+
agent Agent, :required
|
21
22
|
current_context Context, :allow_nil, "The current context of the agent"
|
22
23
|
maximum_command_calls :integer,
|
23
24
|
:allow_nil,
|
@@ -28,23 +29,17 @@ module Foobara
|
|
28
29
|
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
29
30
|
default: "claude-3-7-sonnet-20250219",
|
30
31
|
description: "The model to use for the LLM"
|
31
|
-
log_successful_determine_command_and_inputs_outcomes(
|
32
|
-
:boolean,
|
33
|
-
default: true,
|
34
|
-
description: "You can experiment with turning this off " \
|
35
|
-
"if you want to see what happens if we don't log " \
|
36
|
-
"successful command/input selection outcomes"
|
37
|
-
)
|
38
32
|
choose_next_command_and_next_inputs_separately :boolean,
|
39
33
|
default: false,
|
40
34
|
description:
|
41
35
|
"By default, asks for next command and inputs together. " \
|
42
36
|
"You can experiment with getting the separately " \
|
43
37
|
"with this flag if you wish."
|
38
|
+
max_llm_calls_per_minute :integer, :allow_nil
|
44
39
|
end
|
45
40
|
|
46
41
|
result do
|
47
|
-
message_to_user :string, :
|
42
|
+
message_to_user :string, :allow_nil, "Message to the user about successfully accomplishing the goal"
|
48
43
|
result_data :duck, "Optional result data to return to the user if final_result_type was given"
|
49
44
|
end
|
50
45
|
|
@@ -53,24 +48,16 @@ module Foobara
|
|
53
48
|
def execute
|
54
49
|
build_initial_context_if_necessary
|
55
50
|
|
56
|
-
|
57
|
-
set_accomplished_goal_command
|
58
|
-
else
|
59
|
-
build_command_connector
|
60
|
-
connect_user_provided_commands
|
61
|
-
end
|
62
|
-
|
63
|
-
unless agent_commands_connected?
|
64
|
-
connect_agent_commands
|
65
|
-
end
|
66
|
-
|
51
|
+
simulate_describe_list_commands_command
|
67
52
|
simulate_list_commands_run
|
68
|
-
simulate_describe_command_run_for_all_commands
|
53
|
+
# simulate_describe_command_run_for_all_commands
|
69
54
|
|
70
55
|
until mission_accomplished or given_up
|
71
56
|
increment_command_calls
|
72
57
|
check_if_too_many_calls
|
73
58
|
|
59
|
+
throttle_llm_calls_if_necessary
|
60
|
+
|
74
61
|
if choose_next_command_and_next_inputs_separately?
|
75
62
|
determine_next_command_then_inputs_separately
|
76
63
|
else
|
@@ -88,27 +75,10 @@ module Foobara
|
|
88
75
|
build_result
|
89
76
|
end
|
90
77
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
94
|
-
|
95
|
-
def validate
|
96
|
-
validate_either_command_classes_or_connector_given
|
97
|
-
end
|
98
|
-
|
99
|
-
def validate_either_command_classes_or_connector_given
|
100
|
-
# TODO: implement this!
|
101
|
-
end
|
102
|
-
|
103
|
-
attr_accessor :context, :next_command_name, :next_command_inputs, :mission_accomplished, :given_up,
|
104
|
-
:next_command_class, :next_command, :command_outcome, :timed_out,
|
78
|
+
attr_accessor :context, :next_command_name, :next_command_inputs, :next_command_raw_inputs, :mission_accomplished,
|
79
|
+
:given_up, :next_command_class, :next_command, :command_outcome, :timed_out,
|
105
80
|
:final_result, :final_message, :command_response, :delayed_command_name,
|
106
81
|
:command_calls
|
107
|
-
attr_writer :command_connector
|
108
|
-
|
109
|
-
def agent_name
|
110
|
-
@agent_name ||= inputs[:agent_name] || "Anon#{SecureRandom.hex(2)}"
|
111
|
-
end
|
112
82
|
|
113
83
|
def build_initial_context_if_necessary
|
114
84
|
# TODO: shouldn't have to pass command_log here since it has a default, debug that
|
@@ -116,9 +86,8 @@ module Foobara
|
|
116
86
|
end
|
117
87
|
|
118
88
|
def simulate_list_commands_run
|
119
|
-
return unless context.command_log.empty?
|
120
|
-
|
121
89
|
self.next_command_name = ListCommands.full_command_name
|
90
|
+
self.next_command_raw_inputs = nil
|
122
91
|
self.next_command_inputs = nil
|
123
92
|
fetch_next_command_class
|
124
93
|
|
@@ -126,65 +95,46 @@ module Foobara
|
|
126
95
|
log_last_command_outcome
|
127
96
|
end
|
128
97
|
|
98
|
+
def simulate_describe_list_commands_command
|
99
|
+
self.next_command_name = DescribeCommand.full_command_name
|
100
|
+
self.next_command_inputs = { command_name: ListCommands.full_command_name }
|
101
|
+
self.next_command_raw_inputs = next_command_inputs
|
102
|
+
fetch_next_command_class
|
103
|
+
|
104
|
+
run_next_command
|
105
|
+
log_last_command_outcome
|
106
|
+
end
|
107
|
+
|
129
108
|
def simulate_describe_command_run_for_all_commands
|
109
|
+
# TODO: currently not using this code path. Unclear if it is worth it.
|
110
|
+
# :nocov:
|
130
111
|
return if context.command_log.size > 1
|
131
112
|
|
132
|
-
ListCommands.run!(command_connector:)[:user_provided_commands].each do |full_command_name|
|
113
|
+
ListCommands.run!(command_connector: agent)[:user_provided_commands].each do |full_command_name|
|
133
114
|
next if described_commands.include?(full_command_name)
|
134
115
|
|
135
116
|
self.next_command_name = DescribeCommand.full_command_name
|
136
117
|
self.next_command_inputs = { command_name: full_command_name }
|
118
|
+
self.next_command_raw_inputs = next_command_inputs
|
137
119
|
fetch_next_command_class
|
138
120
|
|
139
121
|
run_next_command
|
140
122
|
log_last_command_outcome
|
141
123
|
end
|
142
|
-
|
143
|
-
|
144
|
-
def command_connector_passed_in?
|
145
|
-
existing_command_connector
|
146
|
-
end
|
147
|
-
|
148
|
-
def command_connector
|
149
|
-
@command_connector ||= existing_command_connector
|
150
|
-
end
|
151
|
-
|
152
|
-
# Do we really want to support calling AccomplishGoal outside the context of an Agent?
|
153
|
-
# If not, just delete this awkward coupling
|
154
|
-
def build_command_connector
|
155
|
-
self.command_connector ||= Agent.new(
|
156
|
-
current_accomplish_goal_command: self,
|
157
|
-
llm_model:,
|
158
|
-
verbose:,
|
159
|
-
io_out:
|
160
|
-
)
|
161
|
-
end
|
162
|
-
|
163
|
-
def set_accomplished_goal_command
|
164
|
-
command_connector.current_accomplish_goal_command = self
|
165
|
-
end
|
166
|
-
|
167
|
-
def connect_agent_commands
|
168
|
-
command_connector.connect_agent_commands(final_result_type:, agent_name:)
|
169
|
-
end
|
170
|
-
|
171
|
-
def connect_user_provided_commands
|
172
|
-
command_classes.each do |command_class|
|
173
|
-
command_connector.connect(command_class)
|
174
|
-
end
|
124
|
+
# :nocov:
|
175
125
|
end
|
176
126
|
|
177
127
|
def determine_next_command_and_inputs(retries = 2)
|
178
128
|
inputs_for_determine = {
|
179
129
|
goal:,
|
180
130
|
context:,
|
181
|
-
llm_model
|
182
|
-
command_class_names: all_command_classes
|
131
|
+
llm_model:
|
183
132
|
}
|
184
133
|
|
185
134
|
determine_command = DetermineNextCommandNameAndInputs.new(inputs_for_determine)
|
186
135
|
|
187
136
|
outcome = begin
|
137
|
+
record_llm_call_timestamp
|
188
138
|
determine_command.run
|
189
139
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
190
140
|
# :nocov:
|
@@ -195,6 +145,7 @@ module Foobara
|
|
195
145
|
if outcome.success?
|
196
146
|
self.next_command_name = outcome.result[:command_name]
|
197
147
|
self.next_command_inputs = outcome.result[:inputs]
|
148
|
+
self.next_command_raw_inputs = next_command_inputs
|
198
149
|
|
199
150
|
outcome = validate_next_command_name
|
200
151
|
|
@@ -204,32 +155,25 @@ module Foobara
|
|
204
155
|
if next_command_has_inputs?
|
205
156
|
outcome = validate_next_command_inputs
|
206
157
|
|
207
|
-
|
208
|
-
if log_successful_determine_command_and_inputs_outcomes?
|
209
|
-
log_command_outcome(
|
210
|
-
command: determine_command,
|
211
|
-
inputs: determine_command.inputs.except(:context)
|
212
|
-
)
|
213
|
-
end
|
214
|
-
else
|
158
|
+
unless outcome.success?
|
215
159
|
log_command_outcome(
|
216
|
-
|
217
|
-
inputs:
|
218
|
-
outcome
|
219
|
-
result: outcome.result || determine_command.raw_result
|
160
|
+
command_name: next_command_name,
|
161
|
+
inputs: next_command_inputs,
|
162
|
+
outcome:
|
220
163
|
)
|
221
164
|
|
222
165
|
determine_next_command_inputs
|
223
166
|
end
|
224
167
|
else
|
225
168
|
self.next_command_inputs = {}
|
169
|
+
self.next_command_raw_inputs = next_command_inputs
|
226
170
|
end
|
227
171
|
else
|
228
172
|
log_command_outcome(
|
229
|
-
|
230
|
-
inputs:
|
173
|
+
command_name: next_command_name,
|
174
|
+
inputs: next_command_inputs,
|
231
175
|
outcome:,
|
232
|
-
result:
|
176
|
+
result: nil
|
233
177
|
)
|
234
178
|
|
235
179
|
if retries > 0
|
@@ -267,10 +211,23 @@ module Foobara
|
|
267
211
|
end
|
268
212
|
|
269
213
|
def validate_next_command_name
|
214
|
+
if next_command_name.is_a?(::String)
|
215
|
+
command_class = agent.transformed_command_from_name(next_command_name)
|
216
|
+
|
217
|
+
if command_class
|
218
|
+
self.next_command_name = command_class.full_command_name
|
219
|
+
|
220
|
+
return Outcome.success(next_command_name)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
270
224
|
outcome = command_name_type.process_value(next_command_name)
|
271
225
|
|
272
226
|
if outcome.success?
|
227
|
+
# TODO: figure out a way to hit this path in the test suite
|
228
|
+
# :nocov:
|
273
229
|
self.next_command_name = outcome.result
|
230
|
+
# :nocov:
|
274
231
|
end
|
275
232
|
|
276
233
|
outcome
|
@@ -294,17 +251,14 @@ module Foobara
|
|
294
251
|
self.delayed_command_name = nil
|
295
252
|
name
|
296
253
|
else
|
297
|
-
command_class = DetermineNextCommand.for(
|
298
|
-
command_class_names: all_command_classes, agent_id: agent_name
|
299
|
-
)
|
300
|
-
|
301
254
|
inputs = { goal:, context: }
|
302
255
|
if llm_model
|
303
256
|
inputs[:llm_model] = llm_model
|
304
257
|
end
|
305
258
|
|
306
|
-
command =
|
259
|
+
command = DetermineNextCommand.new(inputs)
|
307
260
|
outcome = begin
|
261
|
+
record_llm_call_timestamp
|
308
262
|
command.run
|
309
263
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
310
264
|
# :nocov:
|
@@ -313,12 +267,24 @@ module Foobara
|
|
313
267
|
end
|
314
268
|
|
315
269
|
if outcome.success?
|
316
|
-
|
270
|
+
self.next_command_name = outcome.result
|
271
|
+
|
272
|
+
outcome = validate_next_command_name
|
273
|
+
|
274
|
+
unless outcome.success?
|
275
|
+
# TODO: figure out a way to hit this path in the test suite or delete it
|
276
|
+
# :nocov:
|
317
277
|
log_command_outcome(
|
318
278
|
command:,
|
319
279
|
inputs: command.inputs.except(:context),
|
320
|
-
outcome
|
280
|
+
outcome:,
|
281
|
+
result: outcome.result || command.raw_result
|
321
282
|
)
|
283
|
+
|
284
|
+
if retries > 0
|
285
|
+
return determine_next_command_name(retries - 1)
|
286
|
+
end
|
287
|
+
# :nocov:
|
322
288
|
end
|
323
289
|
else
|
324
290
|
# TODO: either figure out a way to hit this path in the test suite or delete it
|
@@ -348,11 +314,11 @@ module Foobara
|
|
348
314
|
end
|
349
315
|
|
350
316
|
def all_command_classes
|
351
|
-
@all_command_classes ||= run_subcommand!(ListCommands, command_connector:).values.flatten
|
317
|
+
@all_command_classes ||= run_subcommand!(ListCommands, command_connector: agent).values.flatten
|
352
318
|
end
|
353
319
|
|
354
320
|
def fetch_next_command_class
|
355
|
-
self.next_command_class =
|
321
|
+
self.next_command_class = agent.transformed_command_from_name(next_command_name)
|
356
322
|
end
|
357
323
|
|
358
324
|
def determine_next_command_inputs(retries = 2)
|
@@ -366,6 +332,7 @@ module Foobara
|
|
366
332
|
|
367
333
|
command = command_class.new(inputs)
|
368
334
|
outcome = begin
|
335
|
+
record_llm_call_timestamp
|
369
336
|
command.run
|
370
337
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
371
338
|
# :nocov:
|
@@ -373,23 +340,15 @@ module Foobara
|
|
373
340
|
# :nocov:
|
374
341
|
end
|
375
342
|
|
376
|
-
|
377
|
-
if log_successful_determine_command_and_inputs_outcomes?
|
378
|
-
log_command_outcome(
|
379
|
-
command:,
|
380
|
-
inputs: command.inputs.except(:context),
|
381
|
-
outcome:
|
382
|
-
)
|
383
|
-
end
|
384
|
-
else
|
343
|
+
unless outcome.success?
|
385
344
|
# TODO: either figure out a way to hit this path in the test suite or delete it
|
386
345
|
# :nocov:
|
387
346
|
log_command_outcome(
|
388
|
-
|
389
|
-
inputs: command.
|
390
|
-
outcome
|
391
|
-
result: outcome.result || command.raw_result
|
347
|
+
command_name: next_command_name,
|
348
|
+
inputs: command.raw_result,
|
349
|
+
outcome:
|
392
350
|
)
|
351
|
+
|
393
352
|
if retries > 0
|
394
353
|
return determine_next_command_inputs(retries - 1)
|
395
354
|
end
|
@@ -414,10 +373,23 @@ module Foobara
|
|
414
373
|
|
415
374
|
def run_next_command
|
416
375
|
if verbose?
|
417
|
-
|
376
|
+
args = if next_command_inputs.nil? || next_command_inputs.empty?
|
377
|
+
""
|
378
|
+
else
|
379
|
+
s = next_command_inputs.to_s
|
380
|
+
|
381
|
+
if s =~ /\A\{(.*)}\z/
|
382
|
+
"(#{::Regexp.last_match(1)})"
|
383
|
+
else
|
384
|
+
# :nocov:
|
385
|
+
raise "Unexpected next_command_inputs: #{next_command_inputs}"
|
386
|
+
# :nocov:
|
387
|
+
end
|
388
|
+
end
|
389
|
+
(io_out || $stdout).puts "#{next_command_name}.run#{args}"
|
418
390
|
end
|
419
391
|
|
420
|
-
self.command_response =
|
392
|
+
self.command_response = agent.run(
|
421
393
|
full_command_name: next_command_name,
|
422
394
|
inputs: next_command_inputs,
|
423
395
|
action: "run"
|
@@ -426,9 +398,7 @@ module Foobara
|
|
426
398
|
self.command_outcome = command_response.outcome
|
427
399
|
|
428
400
|
if verbose?
|
429
|
-
|
430
|
-
(io_out || $stdout).puts "Command #{command_response.command.class.full_command_name} succeeded"
|
431
|
-
else
|
401
|
+
unless command_outcome.success?
|
432
402
|
# :nocov:
|
433
403
|
(io_err || $stderr).puts(
|
434
404
|
"Command #{command_response.command.class.full_command_name} failed #{command_outcome.errors_hash}"
|
@@ -461,8 +431,11 @@ module Foobara
|
|
461
431
|
def log_command_outcome(command: nil, command_name: nil, inputs: nil, outcome: nil, result: nil)
|
462
432
|
if command
|
463
433
|
command_name ||= command.class.full_command_name
|
464
|
-
inputs ||= command.
|
434
|
+
inputs ||= command.raw_inputs
|
465
435
|
outcome ||= command.outcome
|
436
|
+
end
|
437
|
+
|
438
|
+
if outcome
|
466
439
|
result ||= outcome.result
|
467
440
|
end
|
468
441
|
|
@@ -520,17 +493,63 @@ module Foobara
|
|
520
493
|
type.extends_type?(BuiltinTypes[:attributes]) && type.element_types.empty?
|
521
494
|
end
|
522
495
|
|
523
|
-
def log_successful_determine_command_and_inputs_outcomes?
|
524
|
-
log_successful_determine_command_and_inputs_outcomes
|
525
|
-
end
|
526
|
-
|
527
496
|
def choose_next_command_and_next_inputs_separately?
|
528
497
|
choose_next_command_and_next_inputs_separately
|
529
498
|
end
|
530
499
|
|
500
|
+
def agent_name
|
501
|
+
agent.agent_name
|
502
|
+
end
|
503
|
+
|
531
504
|
def verbose?
|
532
505
|
verbose
|
533
506
|
end
|
507
|
+
|
508
|
+
def llm_call_timestamps
|
509
|
+
@llm_call_timestamps ||= []
|
510
|
+
end
|
511
|
+
|
512
|
+
attr_writer :llm_call_timestamps
|
513
|
+
|
514
|
+
def record_llm_call_timestamp
|
515
|
+
llm_call_timestamps.unshift(Time.now)
|
516
|
+
end
|
517
|
+
|
518
|
+
def llm_calls_in_last_minute
|
519
|
+
llm_call_timestamps.select { |t| t > (Time.now - 60) }
|
520
|
+
end
|
521
|
+
|
522
|
+
def llm_call_count_in_last_minute
|
523
|
+
llm_calls_in_last_minute.size
|
524
|
+
end
|
525
|
+
|
526
|
+
def time_until_llm_call_count_in_last_minute_changes
|
527
|
+
calls = llm_calls_in_last_minute
|
528
|
+
|
529
|
+
first_to_expire = calls.first
|
530
|
+
|
531
|
+
if first_to_expire
|
532
|
+
(first_to_expire + SECONDS_PER_MINUTE) - Time.now
|
533
|
+
else
|
534
|
+
# TODO: figure out how to test this code path
|
535
|
+
# :nocov:
|
536
|
+
0
|
537
|
+
# :nocov:
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
def throttle_llm_calls_if_necessary
|
542
|
+
return unless max_llm_calls_per_minute && max_llm_calls_per_minute > 0
|
543
|
+
|
544
|
+
if llm_call_count_in_last_minute >= max_llm_calls_per_minute
|
545
|
+
seconds = time_until_llm_call_count_in_last_minute_changes
|
546
|
+
if verbose?
|
547
|
+
(io_out || $stdout).puts "Sleeping for #{seconds} seconds to avoid LLM calls per minute limit"
|
548
|
+
end
|
549
|
+
|
550
|
+
sleep seconds
|
551
|
+
end
|
552
|
+
end
|
534
553
|
end
|
535
554
|
end
|
536
555
|
end
|
@@ -26,13 +26,20 @@ module Foobara
|
|
26
26
|
end
|
27
27
|
|
28
28
|
if command_class.inputs_type
|
29
|
-
|
29
|
+
transformer = CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer.new(
|
30
|
+
to: command_class.inputs_type
|
31
|
+
)
|
32
|
+
klass.result transformer.from_type
|
30
33
|
end
|
31
34
|
|
32
35
|
klass
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
39
|
+
|
40
|
+
def association_depth
|
41
|
+
Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
|
42
|
+
end
|
36
43
|
end
|
37
44
|
end
|
38
45
|
end
|
@@ -3,45 +3,30 @@ require "foobara/llm_backed_command"
|
|
3
3
|
module Foobara
|
4
4
|
class Agent < CommandConnector
|
5
5
|
class DetermineNextCommand < Foobara::LlmBackedCommand
|
6
|
-
|
6
|
+
description "Accepts the current goal, which might already be accomplished, " \
|
7
|
+
"and context of the work " \
|
8
|
+
"so far and returns the name of " \
|
9
|
+
"the next command to run to make progress towards " \
|
10
|
+
"accomplishing the goal. If the goal has already been accomplished then choose the " \
|
11
|
+
"NotifyUserThatCurrentGoalHasBeenAccomplished command."
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"so far and returns the name of " \
|
18
|
-
"the next command to run to make progress towards " \
|
19
|
-
"accomplishing the goal. If the goal has already been accomplished then choose the " \
|
20
|
-
"NotifyUserThatCurrentGoalHasBeenAccomplished command."
|
21
|
-
|
22
|
-
klass.inputs do
|
23
|
-
goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
|
24
|
-
"by the previous command runs then choose " \
|
25
|
-
"NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
|
26
|
-
context Context, :required, "Context of progress so far"
|
27
|
-
llm_model :string,
|
28
|
-
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
29
|
-
default: "claude-3-7-sonnet-20250219",
|
30
|
-
description: "The model to use for the LLM"
|
31
|
-
end
|
32
|
-
|
33
|
-
klass.result :string,
|
34
|
-
one_of: command_class_names,
|
35
|
-
description: "Name of the next command to run to make progress " \
|
36
|
-
"towards accomplishing the mission"
|
37
|
-
|
38
|
-
klass
|
39
|
-
end
|
40
|
-
end
|
13
|
+
inputs do
|
14
|
+
goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
|
15
|
+
"by the previous command runs then choose " \
|
16
|
+
"NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
|
17
|
+
context Context, :required, "Context of progress so far"
|
18
|
+
llm_model :string,
|
19
|
+
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
20
|
+
default: "claude-3-7-sonnet-20250219",
|
21
|
+
description: "The model to use for the LLM"
|
41
22
|
end
|
42
23
|
|
43
|
-
|
44
|
-
|
24
|
+
result :string,
|
25
|
+
description: "Name of the next command to run to make progress " \
|
26
|
+
"towards accomplishing the mission"
|
27
|
+
def association_depth
|
28
|
+
Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
|
29
|
+
end
|
45
30
|
end
|
46
31
|
end
|
47
32
|
end
|
@@ -14,7 +14,6 @@ module Foobara
|
|
14
14
|
"by the previous command runs then choose " \
|
15
15
|
"NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
|
16
16
|
context Context, :required, "Context of the progress towards the goal so far"
|
17
|
-
command_class_names [:string], :required
|
18
17
|
llm_model :string,
|
19
18
|
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
20
19
|
default: "claude-3-7-sonnet-20250219",
|
@@ -25,6 +24,10 @@ module Foobara
|
|
25
24
|
command_name :string, :required
|
26
25
|
inputs :attributes, :allow_nil
|
27
26
|
end
|
27
|
+
|
28
|
+
def association_depth
|
29
|
+
Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
|
30
|
+
end
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -6,8 +6,10 @@ module Foobara
|
|
6
6
|
class << self
|
7
7
|
attr_accessor :command_class
|
8
8
|
|
9
|
-
def for(result_type
|
10
|
-
|
9
|
+
def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true)
|
10
|
+
agent_id ||= "Anon#{SecureRandom.hex(2)}"
|
11
|
+
|
12
|
+
cached_subclass([result_type, agent_id, include_message_to_user_in_result]) do
|
11
13
|
command_name = "Foobara::Agent::#{agent_id}::NotifyUserThatCurrentGoalHasBeenAccomplished"
|
12
14
|
klass = Util.make_class_p(command_name, self)
|
13
15
|
|
@@ -16,32 +18,50 @@ module Foobara
|
|
16
18
|
"result schema and an optional message to the user. " \
|
17
19
|
"The user might issue a new goal."
|
18
20
|
|
19
|
-
inputs do
|
20
|
-
command_connector CommandConnector, :required, "Connector to notify user through"
|
21
|
-
message_to_user :string, :required, "Message to the user about what was done"
|
22
|
-
end
|
23
|
-
|
24
21
|
if result_type
|
25
|
-
|
26
|
-
|
22
|
+
if include_message_to_user_in_result
|
23
|
+
klass.add_inputs do
|
24
|
+
result result_type, :required
|
25
|
+
message_to_user :string, :required, "Message to the user about what was done"
|
26
|
+
end
|
27
|
+
|
28
|
+
klass.result do
|
29
|
+
result result_type, :required
|
30
|
+
message_to_user :string, :required, "Message to the user about what was done"
|
31
|
+
end
|
32
|
+
|
33
|
+
klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
|
34
|
+
"result formatted according to the " \
|
35
|
+
"result schema and a message to the user. " \
|
36
|
+
"The user might issue a new goal."
|
37
|
+
else
|
38
|
+
klass.add_inputs do
|
39
|
+
result result_type, :required
|
40
|
+
end
|
41
|
+
|
42
|
+
klass.result result_type
|
43
|
+
|
44
|
+
klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
|
45
|
+
"result formatted according to the " \
|
46
|
+
"result schema. " \
|
47
|
+
"The user might issue a new goal."
|
48
|
+
end
|
49
|
+
elsif include_message_to_user_in_result
|
50
|
+
klass.add_inputs do
|
51
|
+
message_to_user :string, :required, "Message to the user about what was done"
|
27
52
|
end
|
28
53
|
|
29
54
|
klass.result do
|
30
|
-
message_to_user :string, :required
|
31
|
-
result_data result_type, :required
|
55
|
+
message_to_user :string, :required, "Message to the user about what was done"
|
32
56
|
end
|
33
|
-
klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
|
34
|
-
"result formatted according to the " \
|
35
|
-
"result schema if relevant and an optional message to the user. " \
|
36
|
-
"The user might issue a new goal."
|
37
57
|
|
58
|
+
klass.description "Notifies the user that the current goal has been accomplished and results in a " \
|
59
|
+
"message to the user. " \
|
60
|
+
"The user might issue a new goal."
|
38
61
|
else
|
39
|
-
#
|
62
|
+
# This should be unreachable actually
|
40
63
|
# :nocov:
|
41
|
-
klass.description "Notifies the user that the current goal has been accomplished
|
42
|
-
"returns a final " \
|
43
|
-
"result formatted according to the " \
|
44
|
-
"result schema and an optional message to the user. " \
|
64
|
+
klass.description "Notifies the user that the current goal has been accomplished. " \
|
45
65
|
"The user might issue a new goal."
|
46
66
|
# :nocov:
|
47
67
|
end
|
@@ -55,10 +75,7 @@ module Foobara
|
|
55
75
|
"result schema if relevant and an optional message to the user."
|
56
76
|
|
57
77
|
inputs do
|
58
|
-
|
59
|
-
command_connector :duck, :required, "Connector to end"
|
60
|
-
message_to_user :string, :required, "Message to the user about what was done"
|
61
|
-
result_data :duck, "The final result of the work if relevant/expected"
|
78
|
+
command_connector CommandConnector, :required, "Connector to notify user through"
|
62
79
|
end
|
63
80
|
|
64
81
|
def execute
|
@@ -68,21 +85,18 @@ module Foobara
|
|
68
85
|
end
|
69
86
|
|
70
87
|
def mark_mission_accomplished
|
71
|
-
|
72
|
-
inputs[:result_data]
|
73
|
-
end
|
74
|
-
|
75
|
-
command_connector.mark_mission_accomplished(data, message_to_user)
|
88
|
+
command_connector.mark_mission_accomplished(inputs[:result], inputs[:message_to_user])
|
76
89
|
end
|
77
90
|
|
78
91
|
def parsed_result
|
79
|
-
|
92
|
+
inputs_type = self.class.inputs_type
|
93
|
+
element_types = inputs_type.element_types
|
80
94
|
|
81
|
-
if
|
82
|
-
|
95
|
+
if element_types.key?(:message_to_user)
|
96
|
+
inputs.slice(:result, :message_to_user)
|
97
|
+
elsif element_types.key?(:result)
|
98
|
+
inputs[:result]
|
83
99
|
end
|
84
|
-
|
85
|
-
h
|
86
100
|
end
|
87
101
|
end
|
88
102
|
end
|
data/src/foobara/agent.rb
CHANGED
@@ -18,10 +18,12 @@ module Foobara
|
|
18
18
|
:llm_model,
|
19
19
|
:current_accomplish_goal_command,
|
20
20
|
:result_type,
|
21
|
+
:include_message_to_user_in_result,
|
21
22
|
:agent_commands_connected,
|
22
23
|
:verbose,
|
23
24
|
:io_out,
|
24
|
-
:io_err
|
25
|
+
:io_err,
|
26
|
+
:max_llm_calls_per_minute
|
25
27
|
|
26
28
|
def initialize(
|
27
29
|
context: nil,
|
@@ -29,27 +31,35 @@ module Foobara
|
|
29
31
|
command_classes: nil,
|
30
32
|
llm_model: nil,
|
31
33
|
result_type: nil,
|
32
|
-
|
34
|
+
include_message_to_user_in_result: true,
|
33
35
|
verbose: false,
|
34
36
|
io_out: nil,
|
35
37
|
io_err: nil,
|
38
|
+
max_llm_calls_per_minute: nil,
|
36
39
|
**opts
|
37
40
|
)
|
38
41
|
# TODO: shouldn't have to pass command_log here since it has a default, debug that
|
39
42
|
self.context = context
|
40
|
-
self.agent_name = agent_name
|
43
|
+
self.agent_name = agent_name || "Anon#{SecureRandom.hex(2)}"
|
41
44
|
self.llm_model = llm_model
|
42
45
|
self.result_type = result_type
|
43
|
-
self.
|
46
|
+
self.include_message_to_user_in_result = include_message_to_user_in_result
|
44
47
|
self.verbose = verbose
|
45
48
|
self.io_out = io_out
|
46
49
|
self.io_err = io_err
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
self.max_llm_calls_per_minute = max_llm_calls_per_minute
|
51
|
+
|
52
|
+
# unless opts.key?(:default_serializers)
|
53
|
+
# opts = opts.merge(default_serializers: [
|
54
|
+
# Foobara::CommandConnectors::Serializers::ErrorsSerializer,
|
55
|
+
# Foobara::CommandConnectors::Serializers::AggregateSerializer
|
56
|
+
# ])
|
57
|
+
# end
|
58
|
+
|
59
|
+
unless opts.key?(:default_pre_commit_transformers)
|
60
|
+
opts = opts.merge(
|
61
|
+
default_pre_commit_transformers: Foobara::CommandConnectors::Transformers::LoadAtomsPreCommitTransformer
|
62
|
+
)
|
53
63
|
end
|
54
64
|
|
55
65
|
super(**opts)
|
@@ -72,9 +82,9 @@ module Foobara
|
|
72
82
|
inputs_transformers = Util.array(inputs_transformers)
|
73
83
|
inputs_transformers << CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer
|
74
84
|
|
75
|
-
unless opts.key?(:aggregate_entities)
|
76
|
-
|
77
|
-
end
|
85
|
+
# unless opts.key?(:aggregate_entities)
|
86
|
+
# opts = opts.merge(aggregate_entities: true)
|
87
|
+
# end
|
78
88
|
|
79
89
|
super(*args, **opts.merge(inputs_transformers:))
|
80
90
|
end
|
@@ -107,7 +117,8 @@ module Foobara
|
|
107
117
|
goal,
|
108
118
|
result_type: nil,
|
109
119
|
choose_next_command_and_next_inputs_separately: nil,
|
110
|
-
maximum_call_count: nil
|
120
|
+
maximum_call_count: nil,
|
121
|
+
llm_model: nil
|
111
122
|
)
|
112
123
|
if result_type && self.result_type != result_type
|
113
124
|
if self.result_type
|
@@ -123,6 +134,10 @@ module Foobara
|
|
123
134
|
end
|
124
135
|
end
|
125
136
|
|
137
|
+
unless agent_commands_connected?
|
138
|
+
connect_agent_commands
|
139
|
+
end
|
140
|
+
|
126
141
|
state_machine.perform_transition!(:accomplish_goal)
|
127
142
|
|
128
143
|
begin
|
@@ -130,12 +145,10 @@ module Foobara
|
|
130
145
|
goal:,
|
131
146
|
final_result_type: self.result_type,
|
132
147
|
current_context: context,
|
133
|
-
|
148
|
+
agent: self
|
134
149
|
}
|
135
150
|
|
136
|
-
|
137
|
-
inputs[:agent_name] = agent_name
|
138
|
-
end
|
151
|
+
llm_model ||= self.llm_model
|
139
152
|
|
140
153
|
if llm_model
|
141
154
|
inputs[:llm_model] = llm_model
|
@@ -161,14 +174,24 @@ module Foobara
|
|
161
174
|
inputs[:io_err] = io_err
|
162
175
|
end
|
163
176
|
|
177
|
+
if include_message_to_user_in_result || include_message_to_user_in_result == false
|
178
|
+
inputs[:include_message_to_user_in_result] = include_message_to_user_in_result
|
179
|
+
end
|
180
|
+
|
181
|
+
if max_llm_calls_per_minute && max_llm_calls_per_minute > 0
|
182
|
+
inputs[:max_llm_calls_per_minute] = max_llm_calls_per_minute
|
183
|
+
end
|
184
|
+
|
164
185
|
self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
|
165
186
|
|
166
187
|
current_accomplish_goal_command.run.tap do |outcome|
|
167
|
-
if outcome.success?
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
188
|
+
transition = if outcome.success?
|
189
|
+
:goal_accomplished
|
190
|
+
else
|
191
|
+
:goal_errored
|
192
|
+
end
|
193
|
+
|
194
|
+
state_machine.perform_transition!(transition)
|
172
195
|
end
|
173
196
|
rescue
|
174
197
|
# :nocov:
|
@@ -197,19 +220,21 @@ module Foobara
|
|
197
220
|
agent_commands_connected
|
198
221
|
end
|
199
222
|
|
200
|
-
def connect_agent_commands
|
223
|
+
def connect_agent_commands
|
201
224
|
command_classes = [
|
202
225
|
DescribeCommand,
|
203
226
|
DescribeType,
|
204
227
|
GiveUp,
|
205
|
-
ListCommands
|
206
|
-
ListTypes
|
228
|
+
ListCommands
|
207
229
|
]
|
208
230
|
|
209
|
-
command_classes << if
|
231
|
+
command_classes << if result_type || include_message_to_user_in_result
|
232
|
+
# TODO: Support changing the final result type when the goal changes
|
210
233
|
NotifyUserThatCurrentGoalHasBeenAccomplished.for(
|
211
|
-
result_type
|
212
|
-
agent_id: agent_name
|
234
|
+
result_type:,
|
235
|
+
agent_id: agent_name,
|
236
|
+
# TODO: Support changing this flag when the goal changes
|
237
|
+
include_message_to_user_in_result:
|
213
238
|
)
|
214
239
|
else
|
215
240
|
NotifyUserThatCurrentGoalHasBeenAccomplished
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foobara-agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miles Georgi
|
@@ -59,7 +59,6 @@ files:
|
|
59
59
|
- src/foobara/agent/determine_next_command_name_and_inputs.rb
|
60
60
|
- src/foobara/agent/give_up.rb
|
61
61
|
- src/foobara/agent/list_commands.rb
|
62
|
-
- src/foobara/agent/list_types.rb
|
63
62
|
- src/foobara/agent/notify_user_that_current_goal_has_been_accomplished.rb
|
64
63
|
- src/foobara/agent/types/command_log_entry.rb
|
65
64
|
- src/foobara/agent/types/context.rb
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module Foobara
|
2
|
-
class Agent < CommandConnector
|
3
|
-
class ListTypes < Foobara::Command
|
4
|
-
inputs do
|
5
|
-
command_connector :duck, :required, "Connector to fetch types from"
|
6
|
-
end
|
7
|
-
|
8
|
-
result [:string]
|
9
|
-
|
10
|
-
def execute
|
11
|
-
construct_type_list
|
12
|
-
|
13
|
-
type_list
|
14
|
-
end
|
15
|
-
|
16
|
-
attr_accessor :type_list
|
17
|
-
|
18
|
-
def construct_type_list
|
19
|
-
self.type_list = command_connector.all_exposed_type_names
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|