foobara-agent 0.0.7 → 0.0.9
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 +16 -1
- data/lib/foobara/agent.rb +0 -1
- data/src/foobara/agent/accomplish_goal.rb +215 -70
- data/src/foobara/agent/determine_base.rb +82 -0
- data/src/foobara/agent/determine_inputs_for_next_command.rb +11 -16
- data/src/foobara/agent/determine_next_command.rb +8 -42
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +5 -18
- data/src/foobara/agent/types/context.rb +14 -0
- data/src/foobara/agent.rb +32 -16
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c086912a26df48a558f040a3b2e03138210674ec371f528437f08b007ba31783
|
4
|
+
data.tar.gz: 86315598bc4ca17d59cfc6e2d204561c2b387c0162a19601281f3825a7e68337
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ce3b72dca0e4e111c8a68f61d9c80eeabbfcfd480d858ebeaa92b9e7cc4730fd4278654ce7b3ded885628297aa0eb8684ee73b7213559e9d9f93760a6c06086
|
7
|
+
data.tar.gz: 28c540d5663172ff9d15925f152357872576d96e275bb278edc5c6661c47dfbe9fc0832fec9268fe1b215dfaeb5d43d30eef422bc572dc4e9023e82cd77c3a79
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
|
+
## [0.0.9] - 2025-06-28
|
2
|
+
|
3
|
+
- Relocate some common Determine* behavior into a DetermineBase
|
4
|
+
- Move goal to Context and make use of LlmBackedCommand#messages
|
5
|
+
- Simulating a DescribeCommand selection on failure
|
6
|
+
- Compacting the command log
|
7
|
+
|
8
|
+
## [0.0.8] - 2025-06-27
|
9
|
+
|
10
|
+
- Improve what is logged and its formatting when verbose
|
11
|
+
- Improve/experiment with what is stored in the command log
|
12
|
+
- Experiment with Atoms instead of Aggregates
|
13
|
+
- Add max_llm_calls_per_minute option
|
14
|
+
|
1
15
|
## [0.0.7] - 2025-06-23
|
2
16
|
|
3
|
-
-
|
17
|
+
- Improvements to result type handling
|
18
|
+
- Eliminate DescribeType command
|
4
19
|
|
5
20
|
## [0.0.6] - 2025-06-19
|
6
21
|
|
data/lib/foobara/agent.rb
CHANGED
@@ -3,6 +3,9 @@ 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 }
|
@@ -16,7 +19,7 @@ module Foobara
|
|
16
19
|
io_out :duck
|
17
20
|
io_err :duck
|
18
21
|
agent Agent, :required
|
19
|
-
|
22
|
+
context Context, :required, "The current context of the agent"
|
20
23
|
maximum_command_calls :integer,
|
21
24
|
:allow_nil,
|
22
25
|
default: 25,
|
@@ -26,19 +29,13 @@ module Foobara
|
|
26
29
|
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
27
30
|
default: "claude-3-7-sonnet-20250219",
|
28
31
|
description: "The model to use for the LLM"
|
29
|
-
log_successful_determine_command_and_inputs_outcomes(
|
30
|
-
:boolean,
|
31
|
-
default: true,
|
32
|
-
description: "You can experiment with turning this off " \
|
33
|
-
"if you want to see what happens if we don't log " \
|
34
|
-
"successful command/input selection outcomes"
|
35
|
-
)
|
36
32
|
choose_next_command_and_next_inputs_separately :boolean,
|
37
33
|
default: false,
|
38
34
|
description:
|
39
35
|
"By default, asks for next command and inputs together. " \
|
40
36
|
"You can experiment with getting the separately " \
|
41
37
|
"with this flag if you wish."
|
38
|
+
max_llm_calls_per_minute :integer, :allow_nil
|
42
39
|
end
|
43
40
|
|
44
41
|
result do
|
@@ -49,15 +46,16 @@ module Foobara
|
|
49
46
|
depends_on ListCommands
|
50
47
|
|
51
48
|
def execute
|
52
|
-
|
53
|
-
|
49
|
+
simulate_describe_list_commands_command
|
54
50
|
simulate_list_commands_run
|
55
|
-
simulate_describe_command_run_for_all_commands
|
51
|
+
# simulate_describe_command_run_for_all_commands
|
56
52
|
|
57
53
|
until mission_accomplished or given_up
|
58
54
|
increment_command_calls
|
59
55
|
check_if_too_many_calls
|
60
56
|
|
57
|
+
throttle_llm_calls_if_necessary
|
58
|
+
|
61
59
|
if choose_next_command_and_next_inputs_separately?
|
62
60
|
determine_next_command_then_inputs_separately
|
63
61
|
else
|
@@ -66,6 +64,7 @@ module Foobara
|
|
66
64
|
|
67
65
|
run_next_command
|
68
66
|
log_last_command_outcome
|
67
|
+
compact_command_log
|
69
68
|
end
|
70
69
|
|
71
70
|
if given_up
|
@@ -75,20 +74,14 @@ module Foobara
|
|
75
74
|
build_result
|
76
75
|
end
|
77
76
|
|
78
|
-
attr_accessor :
|
79
|
-
:next_command_class, :next_command, :command_outcome, :timed_out,
|
77
|
+
attr_accessor :next_command_name, :next_command_inputs, :next_command_raw_inputs, :mission_accomplished,
|
78
|
+
:given_up, :next_command_class, :next_command, :command_outcome, :timed_out,
|
80
79
|
:final_result, :final_message, :command_response, :delayed_command_name,
|
81
80
|
:command_calls
|
82
81
|
|
83
|
-
def build_initial_context_if_necessary
|
84
|
-
# TODO: shouldn't have to pass command_log here since it has a default, debug that
|
85
|
-
self.context = current_context || Context.new(command_log: [])
|
86
|
-
end
|
87
|
-
|
88
82
|
def simulate_list_commands_run
|
89
|
-
return unless context.command_log.empty?
|
90
|
-
|
91
83
|
self.next_command_name = ListCommands.full_command_name
|
84
|
+
self.next_command_raw_inputs = nil
|
92
85
|
self.next_command_inputs = nil
|
93
86
|
fetch_next_command_class
|
94
87
|
|
@@ -96,7 +89,39 @@ module Foobara
|
|
96
89
|
log_last_command_outcome
|
97
90
|
end
|
98
91
|
|
92
|
+
def simulate_describe_list_commands_command
|
93
|
+
self.next_command_name = DescribeCommand.full_command_name
|
94
|
+
self.next_command_inputs = { command_name: ListCommands.full_command_name }
|
95
|
+
self.next_command_raw_inputs = next_command_inputs
|
96
|
+
fetch_next_command_class
|
97
|
+
|
98
|
+
run_next_command
|
99
|
+
log_last_command_outcome
|
100
|
+
end
|
101
|
+
|
102
|
+
def simulate_describe_command(command_name = next_command_name)
|
103
|
+
old_next_command_name = next_command_name
|
104
|
+
old_next_command_inputs = next_command_inputs
|
105
|
+
old_next_command_raw_inputs = next_command_raw_inputs
|
106
|
+
old_next_command_class = next_command_class
|
107
|
+
|
108
|
+
self.next_command_name = DescribeCommand.full_command_name
|
109
|
+
self.next_command_inputs = { command_name: }
|
110
|
+
self.next_command_raw_inputs = next_command_inputs
|
111
|
+
fetch_next_command_class
|
112
|
+
|
113
|
+
run_next_command
|
114
|
+
log_last_command_outcome
|
115
|
+
|
116
|
+
self.next_command_name = old_next_command_name
|
117
|
+
self.next_command_inputs = old_next_command_inputs
|
118
|
+
self.next_command_raw_inputs = old_next_command_raw_inputs
|
119
|
+
self.next_command_class = old_next_command_class
|
120
|
+
end
|
121
|
+
|
99
122
|
def simulate_describe_command_run_for_all_commands
|
123
|
+
# TODO: currently not using this code path. Unclear if it is worth it.
|
124
|
+
# :nocov:
|
100
125
|
return if context.command_log.size > 1
|
101
126
|
|
102
127
|
ListCommands.run!(command_connector: agent)[:user_provided_commands].each do |full_command_name|
|
@@ -104,24 +129,25 @@ module Foobara
|
|
104
129
|
|
105
130
|
self.next_command_name = DescribeCommand.full_command_name
|
106
131
|
self.next_command_inputs = { command_name: full_command_name }
|
132
|
+
self.next_command_raw_inputs = next_command_inputs
|
107
133
|
fetch_next_command_class
|
108
134
|
|
109
135
|
run_next_command
|
110
136
|
log_last_command_outcome
|
111
137
|
end
|
138
|
+
# :nocov:
|
112
139
|
end
|
113
140
|
|
114
141
|
def determine_next_command_and_inputs(retries = 2)
|
115
142
|
inputs_for_determine = {
|
116
|
-
goal:,
|
117
143
|
context:,
|
118
|
-
llm_model
|
119
|
-
command_class_names: all_command_classes
|
144
|
+
llm_model:
|
120
145
|
}
|
121
146
|
|
122
147
|
determine_command = DetermineNextCommandNameAndInputs.new(inputs_for_determine)
|
123
148
|
|
124
149
|
outcome = begin
|
150
|
+
record_llm_call_timestamp
|
125
151
|
determine_command.run
|
126
152
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
127
153
|
# :nocov:
|
@@ -132,6 +158,7 @@ module Foobara
|
|
132
158
|
if outcome.success?
|
133
159
|
self.next_command_name = outcome.result[:command_name]
|
134
160
|
self.next_command_inputs = outcome.result[:inputs]
|
161
|
+
self.next_command_raw_inputs = next_command_inputs
|
135
162
|
|
136
163
|
outcome = validate_next_command_name
|
137
164
|
|
@@ -141,32 +168,26 @@ module Foobara
|
|
141
168
|
if next_command_has_inputs?
|
142
169
|
outcome = validate_next_command_inputs
|
143
170
|
|
144
|
-
|
145
|
-
if log_successful_determine_command_and_inputs_outcomes?
|
146
|
-
log_command_outcome(
|
147
|
-
command: determine_command,
|
148
|
-
inputs: determine_command.inputs.except(:context)
|
149
|
-
)
|
150
|
-
end
|
151
|
-
else
|
171
|
+
unless outcome.success?
|
152
172
|
log_command_outcome(
|
153
|
-
|
154
|
-
inputs:
|
155
|
-
outcome
|
156
|
-
result: outcome.result || determine_command.raw_result
|
173
|
+
command_name: next_command_name,
|
174
|
+
inputs: next_command_inputs,
|
175
|
+
outcome:
|
157
176
|
)
|
158
177
|
|
178
|
+
simulate_describe_command
|
159
179
|
determine_next_command_inputs
|
160
180
|
end
|
161
181
|
else
|
162
182
|
self.next_command_inputs = {}
|
183
|
+
self.next_command_raw_inputs = next_command_inputs
|
163
184
|
end
|
164
185
|
else
|
165
186
|
log_command_outcome(
|
166
|
-
|
167
|
-
inputs:
|
187
|
+
command_name: next_command_name,
|
188
|
+
inputs: next_command_inputs,
|
168
189
|
outcome:,
|
169
|
-
result:
|
190
|
+
result: nil
|
170
191
|
)
|
171
192
|
|
172
193
|
if retries > 0
|
@@ -204,10 +225,23 @@ module Foobara
|
|
204
225
|
end
|
205
226
|
|
206
227
|
def validate_next_command_name
|
228
|
+
if next_command_name.is_a?(::String)
|
229
|
+
command_class = agent.transformed_command_from_name(next_command_name)
|
230
|
+
|
231
|
+
if command_class
|
232
|
+
self.next_command_name = command_class.full_command_name
|
233
|
+
|
234
|
+
return Outcome.success(next_command_name)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
207
238
|
outcome = command_name_type.process_value(next_command_name)
|
208
239
|
|
209
240
|
if outcome.success?
|
241
|
+
# TODO: figure out a way to hit this path in the test suite
|
242
|
+
# :nocov:
|
210
243
|
self.next_command_name = outcome.result
|
244
|
+
# :nocov:
|
211
245
|
end
|
212
246
|
|
213
247
|
outcome
|
@@ -231,17 +265,14 @@ module Foobara
|
|
231
265
|
self.delayed_command_name = nil
|
232
266
|
name
|
233
267
|
else
|
234
|
-
|
235
|
-
command_class_names: all_command_classes, agent_id: agent_name
|
236
|
-
)
|
237
|
-
|
238
|
-
inputs = { goal:, context: }
|
268
|
+
inputs = { context: }
|
239
269
|
if llm_model
|
240
270
|
inputs[:llm_model] = llm_model
|
241
271
|
end
|
242
272
|
|
243
|
-
command =
|
273
|
+
command = DetermineNextCommand.new(inputs)
|
244
274
|
outcome = begin
|
275
|
+
record_llm_call_timestamp
|
245
276
|
command.run
|
246
277
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
247
278
|
# :nocov:
|
@@ -250,19 +281,31 @@ module Foobara
|
|
250
281
|
end
|
251
282
|
|
252
283
|
if outcome.success?
|
253
|
-
|
284
|
+
self.next_command_name = outcome.result
|
285
|
+
|
286
|
+
outcome = validate_next_command_name
|
287
|
+
|
288
|
+
unless outcome.success?
|
289
|
+
# TODO: figure out a way to hit this path in the test suite or delete it
|
290
|
+
# :nocov:
|
254
291
|
log_command_outcome(
|
255
292
|
command:,
|
256
293
|
inputs: command.inputs.except(:context),
|
257
|
-
outcome
|
294
|
+
outcome:,
|
295
|
+
result: outcome.result || command.raw_result
|
258
296
|
)
|
297
|
+
|
298
|
+
if retries > 0
|
299
|
+
return determine_next_command_name(retries - 1)
|
300
|
+
end
|
301
|
+
# :nocov:
|
259
302
|
end
|
260
303
|
else
|
261
304
|
# TODO: either figure out a way to hit this path in the test suite or delete it
|
262
305
|
# :nocov:
|
263
306
|
log_command_outcome(
|
264
307
|
command:,
|
265
|
-
inputs: command.inputs
|
308
|
+
inputs: command.inputs&.except(:context),
|
266
309
|
outcome:,
|
267
310
|
result: outcome.result || command.raw_result
|
268
311
|
)
|
@@ -296,13 +339,14 @@ module Foobara
|
|
296
339
|
self.next_command_inputs = if next_command_has_inputs?
|
297
340
|
command_class = command_class_for_determine_inputs_for_next_command
|
298
341
|
|
299
|
-
inputs = {
|
342
|
+
inputs = { context: }
|
300
343
|
if llm_model
|
301
344
|
inputs[:llm_model] = llm_model
|
302
345
|
end
|
303
346
|
|
304
347
|
command = command_class.new(inputs)
|
305
348
|
outcome = begin
|
349
|
+
record_llm_call_timestamp
|
306
350
|
command.run
|
307
351
|
rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
|
308
352
|
# :nocov:
|
@@ -310,23 +354,17 @@ module Foobara
|
|
310
354
|
# :nocov:
|
311
355
|
end
|
312
356
|
|
313
|
-
|
314
|
-
if log_successful_determine_command_and_inputs_outcomes?
|
315
|
-
log_command_outcome(
|
316
|
-
command:,
|
317
|
-
inputs: command.inputs.except(:context),
|
318
|
-
outcome:
|
319
|
-
)
|
320
|
-
end
|
321
|
-
else
|
357
|
+
unless outcome.success?
|
322
358
|
# TODO: either figure out a way to hit this path in the test suite or delete it
|
323
359
|
# :nocov:
|
324
360
|
log_command_outcome(
|
325
|
-
|
326
|
-
inputs: command.
|
327
|
-
outcome
|
328
|
-
result: outcome.result || command.raw_result
|
361
|
+
command_name: next_command_name,
|
362
|
+
inputs: command.raw_result,
|
363
|
+
outcome:
|
329
364
|
)
|
365
|
+
|
366
|
+
simulate_describe_command
|
367
|
+
|
330
368
|
if retries > 0
|
331
369
|
return determine_next_command_inputs(retries - 1)
|
332
370
|
end
|
@@ -351,7 +389,20 @@ module Foobara
|
|
351
389
|
|
352
390
|
def run_next_command
|
353
391
|
if verbose?
|
354
|
-
|
392
|
+
args = if next_command_inputs.nil? || next_command_inputs.empty?
|
393
|
+
""
|
394
|
+
else
|
395
|
+
s = next_command_inputs.to_s
|
396
|
+
|
397
|
+
if s =~ /\A\{(.*)}\z/
|
398
|
+
"(#{::Regexp.last_match(1)})"
|
399
|
+
else
|
400
|
+
# :nocov:
|
401
|
+
raise "Unexpected next_command_inputs: #{next_command_inputs}"
|
402
|
+
# :nocov:
|
403
|
+
end
|
404
|
+
end
|
405
|
+
(io_out || $stdout).puts "#{next_command_name}.run#{args}"
|
355
406
|
end
|
356
407
|
|
357
408
|
self.command_response = agent.run(
|
@@ -363,9 +414,7 @@ module Foobara
|
|
363
414
|
self.command_outcome = command_response.outcome
|
364
415
|
|
365
416
|
if verbose?
|
366
|
-
|
367
|
-
(io_out || $stdout).puts "Command #{command_response.command.class.full_command_name} succeeded"
|
368
|
-
else
|
417
|
+
unless command_outcome.success?
|
369
418
|
# :nocov:
|
370
419
|
(io_err || $stderr).puts(
|
371
420
|
"Command #{command_response.command.class.full_command_name} failed #{command_outcome.errors_hash}"
|
@@ -379,6 +428,57 @@ module Foobara
|
|
379
428
|
log_command_outcome(command: command_response.command)
|
380
429
|
end
|
381
430
|
|
431
|
+
def compact_command_log
|
432
|
+
# Rules:
|
433
|
+
# Delete errors for any command that has succeeded since
|
434
|
+
# Delete all but the last DescribeCommand call
|
435
|
+
describe_command_call_indexes = []
|
436
|
+
commands = {}
|
437
|
+
|
438
|
+
describe_command_name = DescribeCommand.full_command_name
|
439
|
+
context.command_log.each.with_index do |command_log_entry, index|
|
440
|
+
command_name = command_log_entry.command_name
|
441
|
+
|
442
|
+
if command_name == describe_command_name
|
443
|
+
describe_command_call_indexes << index
|
444
|
+
end
|
445
|
+
|
446
|
+
commands[command_name] ||= [[], []]
|
447
|
+
bucket_index = command_log_entry.outcome[:success] ? 0 : 1
|
448
|
+
commands[command_name][bucket_index] << index
|
449
|
+
end
|
450
|
+
|
451
|
+
indexes_to_delete = describe_command_call_indexes[0..-2]
|
452
|
+
|
453
|
+
commands.each_value do |(success_indexes, failure_indexes)|
|
454
|
+
last_success = success_indexes.last
|
455
|
+
next unless last_success
|
456
|
+
|
457
|
+
failure_indexes.each do |failure_index|
|
458
|
+
if failure_index < last_success
|
459
|
+
indexes_to_delete << failure_index
|
460
|
+
else
|
461
|
+
# :nocov:
|
462
|
+
break
|
463
|
+
# :nocov:
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
if indexes_to_delete.empty?
|
469
|
+
return
|
470
|
+
end
|
471
|
+
|
472
|
+
new_log = []
|
473
|
+
context.command_log = context.command_log.each.with_index do |entry, index|
|
474
|
+
unless indexes_to_delete.include?(index)
|
475
|
+
new_log << entry
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
context.command_log = new_log
|
480
|
+
end
|
481
|
+
|
382
482
|
def increment_command_calls
|
383
483
|
self.command_calls ||= -1
|
384
484
|
self.command_calls += 1
|
@@ -398,8 +498,11 @@ module Foobara
|
|
398
498
|
def log_command_outcome(command: nil, command_name: nil, inputs: nil, outcome: nil, result: nil)
|
399
499
|
if command
|
400
500
|
command_name ||= command.class.full_command_name
|
401
|
-
inputs ||= command.
|
501
|
+
inputs ||= command.raw_inputs
|
402
502
|
outcome ||= command.outcome
|
503
|
+
end
|
504
|
+
|
505
|
+
if outcome&.success?
|
403
506
|
result ||= outcome.result
|
404
507
|
end
|
405
508
|
|
@@ -457,10 +560,6 @@ module Foobara
|
|
457
560
|
type.extends_type?(BuiltinTypes[:attributes]) && type.element_types.empty?
|
458
561
|
end
|
459
562
|
|
460
|
-
def log_successful_determine_command_and_inputs_outcomes?
|
461
|
-
log_successful_determine_command_and_inputs_outcomes
|
462
|
-
end
|
463
|
-
|
464
563
|
def choose_next_command_and_next_inputs_separately?
|
465
564
|
choose_next_command_and_next_inputs_separately
|
466
565
|
end
|
@@ -472,6 +571,52 @@ module Foobara
|
|
472
571
|
def verbose?
|
473
572
|
verbose
|
474
573
|
end
|
574
|
+
|
575
|
+
def llm_call_timestamps
|
576
|
+
@llm_call_timestamps ||= []
|
577
|
+
end
|
578
|
+
|
579
|
+
attr_writer :llm_call_timestamps
|
580
|
+
|
581
|
+
def record_llm_call_timestamp
|
582
|
+
llm_call_timestamps.unshift(Time.now)
|
583
|
+
end
|
584
|
+
|
585
|
+
def llm_calls_in_last_minute
|
586
|
+
llm_call_timestamps.select { |t| t > (Time.now - 60) }
|
587
|
+
end
|
588
|
+
|
589
|
+
def llm_call_count_in_last_minute
|
590
|
+
llm_calls_in_last_minute.size
|
591
|
+
end
|
592
|
+
|
593
|
+
def time_until_llm_call_count_in_last_minute_changes
|
594
|
+
calls = llm_calls_in_last_minute
|
595
|
+
|
596
|
+
first_to_expire = calls.first
|
597
|
+
|
598
|
+
if first_to_expire
|
599
|
+
(first_to_expire + SECONDS_PER_MINUTE) - Time.now
|
600
|
+
else
|
601
|
+
# TODO: figure out how to test this code path
|
602
|
+
# :nocov:
|
603
|
+
0
|
604
|
+
# :nocov:
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
def throttle_llm_calls_if_necessary
|
609
|
+
return unless max_llm_calls_per_minute && max_llm_calls_per_minute > 0
|
610
|
+
|
611
|
+
if llm_call_count_in_last_minute >= max_llm_calls_per_minute
|
612
|
+
seconds = time_until_llm_call_count_in_last_minute_changes
|
613
|
+
if verbose?
|
614
|
+
(io_out || $stdout).puts "Sleeping for #{seconds} seconds to avoid LLM calls per minute limit"
|
615
|
+
end
|
616
|
+
|
617
|
+
sleep seconds
|
618
|
+
end
|
619
|
+
end
|
475
620
|
end
|
476
621
|
end
|
477
622
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "foobara/llm_backed_command"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
class Agent < CommandConnector
|
5
|
+
class DetermineBase < Foobara::LlmBackedCommand
|
6
|
+
inputs do
|
7
|
+
context Context, :required, "Context of the progress towards the goal so far"
|
8
|
+
llm_model :string,
|
9
|
+
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
10
|
+
default: "claude-3-7-sonnet-20250219",
|
11
|
+
description: "The model to use for the LLM"
|
12
|
+
end
|
13
|
+
|
14
|
+
def association_depth
|
15
|
+
Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_messages
|
19
|
+
p = [
|
20
|
+
{
|
21
|
+
content: llm_instructions,
|
22
|
+
role: :system
|
23
|
+
}
|
24
|
+
]
|
25
|
+
|
26
|
+
context.command_log.each do |command_log_entry|
|
27
|
+
agent_entry = {
|
28
|
+
command: command_log_entry.command_name
|
29
|
+
}
|
30
|
+
|
31
|
+
inputs = command_log_entry.inputs
|
32
|
+
|
33
|
+
if inputs && !inputs.empty?
|
34
|
+
agent_entry[:inputs] = inputs
|
35
|
+
end
|
36
|
+
|
37
|
+
outcome_entry = command_log_entry.outcome
|
38
|
+
|
39
|
+
p << {
|
40
|
+
content: agent_entry,
|
41
|
+
role: :assistant
|
42
|
+
}
|
43
|
+
p << {
|
44
|
+
content: outcome_entry,
|
45
|
+
role: :user
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
p
|
50
|
+
end
|
51
|
+
|
52
|
+
def llm_instructions
|
53
|
+
return @llm_instructions if defined?(@llm_instructions)
|
54
|
+
|
55
|
+
description = self.class.description
|
56
|
+
|
57
|
+
instructions = "You are the implementation of a command called #{self.class.scoped_full_name}"
|
58
|
+
|
59
|
+
instructions += if description && !description.empty?
|
60
|
+
" which has the following description:\n\n#{self.class.description}\n\n"
|
61
|
+
else
|
62
|
+
# :nocov:
|
63
|
+
". "
|
64
|
+
# :nocov:
|
65
|
+
end
|
66
|
+
|
67
|
+
instructions += "You are working towards accomplishing the following goal:\n\n#{goal}\n\n"
|
68
|
+
instructions += "Your response should match the following JSON schema: \n\n#{self.class.result_json_schema}\n\n"
|
69
|
+
instructions += "You can get more details about the result schema for a specific command by " \
|
70
|
+
"choosing the DescribeCommand command. " \
|
71
|
+
"You will reply with nothing more than the JSON you've generated so that the calling code " \
|
72
|
+
"can successfully parse your answer."
|
73
|
+
|
74
|
+
@llm_instructions = instructions
|
75
|
+
end
|
76
|
+
|
77
|
+
def goal
|
78
|
+
context.current_goal
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,8 +1,6 @@
|
|
1
|
-
require "foobara/llm_backed_command"
|
2
|
-
|
3
1
|
module Foobara
|
4
2
|
class Agent < CommandConnector
|
5
|
-
class DetermineInputsForNextCommand <
|
3
|
+
class DetermineInputsForNextCommand < DetermineBase
|
6
4
|
extend Concerns::SubclassCacheable
|
7
5
|
|
8
6
|
class << self
|
@@ -12,22 +10,19 @@ module Foobara
|
|
12
10
|
class_name = "Foobara::Agent::#{agent_id}::DetermineInputsForNext#{command_short_name}Command"
|
13
11
|
klass = Util.make_class_p(class_name, self)
|
14
12
|
|
15
|
-
klass.description "
|
16
|
-
"the next #{
|
17
|
-
"accomplishing the goal."
|
13
|
+
klass.description "Returns the inputs for " \
|
14
|
+
"the next #{command_class.full_command_name} command to run."
|
18
15
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
24
|
-
default: "claude-3-7-sonnet-20250219",
|
25
|
-
description: "The model to use for the LLM"
|
16
|
+
if command_class.inputs_type.nil? || command_class.inputs_type.element_types.empty?
|
17
|
+
# :nocov:
|
18
|
+
raise ArgumentError, "command #{command_class.full_command_name} has no inputs"
|
19
|
+
# :nocov:
|
26
20
|
end
|
27
21
|
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
transformer = CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer.new(
|
23
|
+
to: command_class.inputs_type
|
24
|
+
)
|
25
|
+
klass.result transformer.from_type
|
31
26
|
|
32
27
|
klass
|
33
28
|
end
|
@@ -1,47 +1,13 @@
|
|
1
|
-
require "foobara/llm_backed_command"
|
2
|
-
|
3
1
|
module Foobara
|
4
2
|
class Agent < CommandConnector
|
5
|
-
class DetermineNextCommand <
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
klass = Util.make_class_p(command_name, self)
|
14
|
-
|
15
|
-
klass.description "Accepts the current goal, which might already be accomplished, " \
|
16
|
-
"and context of the work " \
|
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
|
41
|
-
end
|
42
|
-
|
43
|
-
description "Accepts a goal and context of the work so far and returns the name of the next command to run to " \
|
44
|
-
"make progress towards accomplishing the mission."
|
3
|
+
class DetermineNextCommand < DetermineBase
|
4
|
+
description "Returns the name of the next command to run given the progress " \
|
5
|
+
"towards accomplishing the current goal. " \
|
6
|
+
"If the goal has been accomplished it will choose the " \
|
7
|
+
"NotifyUserThatCurrentGoalHasBeenAccomplished command."
|
8
|
+
|
9
|
+
result :string,
|
10
|
+
description: "Name of the next command to run"
|
45
11
|
end
|
46
12
|
end
|
47
13
|
end
|
@@ -1,26 +1,13 @@
|
|
1
|
-
|
1
|
+
require_relative "determine_base"
|
2
2
|
|
3
3
|
module Foobara
|
4
4
|
class Agent < CommandConnector
|
5
|
-
class DetermineNextCommandNameAndInputs <
|
6
|
-
description "
|
7
|
-
"
|
8
|
-
"the
|
9
|
-
"accomplishing the goal. If the goal has already been accomplished then choose the " \
|
5
|
+
class DetermineNextCommandNameAndInputs < DetermineBase
|
6
|
+
description "Returns the name of the next command to run and its inputs given the progress " \
|
7
|
+
"towards accomplishing the current goal. " \
|
8
|
+
"If the goal has been accomplished it will choose the " \
|
10
9
|
"NotifyUserThatCurrentGoalHasBeenAccomplished command."
|
11
10
|
|
12
|
-
inputs do
|
13
|
-
goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
|
14
|
-
"by the previous command runs then choose " \
|
15
|
-
"NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
|
16
|
-
context Context, :required, "Context of the progress towards the goal so far"
|
17
|
-
command_class_names [:string], :required
|
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"
|
22
|
-
end
|
23
|
-
|
24
11
|
result do
|
25
12
|
command_name :string, :required
|
26
13
|
inputs :attributes, :allow_nil
|
@@ -1,11 +1,25 @@
|
|
1
1
|
module Foobara
|
2
2
|
class Agent < CommandConnector
|
3
3
|
class Context < Foobara::Model
|
4
|
+
class << self
|
5
|
+
def for(goal)
|
6
|
+
new(command_log: [], current_goal: goal)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
4
10
|
attributes do
|
11
|
+
current_goal :string, :required, "The current goal the agent needs to accomplish"
|
12
|
+
previous_goals [:string]
|
5
13
|
# TODO: why doesn't this default of [] work as expected on newly created models?
|
6
14
|
command_log [CommandLogEntry], default: [],
|
7
15
|
description: "Log of all commands run so far and their outcomes"
|
8
16
|
end
|
17
|
+
|
18
|
+
def set_new_goal(goal)
|
19
|
+
self.previous_goals ||= []
|
20
|
+
previous_goals << current_goal
|
21
|
+
self.current_goal = goal
|
22
|
+
end
|
9
23
|
end
|
10
24
|
end
|
11
25
|
end
|
data/src/foobara/agent.rb
CHANGED
@@ -22,7 +22,8 @@ module Foobara
|
|
22
22
|
:agent_commands_connected,
|
23
23
|
:verbose,
|
24
24
|
:io_out,
|
25
|
-
:io_err
|
25
|
+
:io_err,
|
26
|
+
:max_llm_calls_per_minute
|
26
27
|
|
27
28
|
def initialize(
|
28
29
|
context: nil,
|
@@ -34,6 +35,7 @@ module Foobara
|
|
34
35
|
verbose: false,
|
35
36
|
io_out: nil,
|
36
37
|
io_err: nil,
|
38
|
+
max_llm_calls_per_minute: nil,
|
37
39
|
**opts
|
38
40
|
)
|
39
41
|
# TODO: shouldn't have to pass command_log here since it has a default, debug that
|
@@ -45,12 +47,19 @@ module Foobara
|
|
45
47
|
self.verbose = verbose
|
46
48
|
self.io_out = io_out
|
47
49
|
self.io_err = io_err
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
+
)
|
54
63
|
end
|
55
64
|
|
56
65
|
super(**opts)
|
@@ -58,8 +67,6 @@ module Foobara
|
|
58
67
|
# TODO: this should work now, switch to this approach
|
59
68
|
# add_default_inputs_transformer EntityToPrimaryKeyInputsTransformer
|
60
69
|
|
61
|
-
build_initial_context
|
62
|
-
|
63
70
|
# TODO: push this convenience method up into base class?
|
64
71
|
command_classes&.each do |command_class|
|
65
72
|
connect(command_class)
|
@@ -73,9 +80,9 @@ module Foobara
|
|
73
80
|
inputs_transformers = Util.array(inputs_transformers)
|
74
81
|
inputs_transformers << CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer
|
75
82
|
|
76
|
-
unless opts.key?(:aggregate_entities)
|
77
|
-
|
78
|
-
end
|
83
|
+
# unless opts.key?(:aggregate_entities)
|
84
|
+
# opts = opts.merge(aggregate_entities: true)
|
85
|
+
# end
|
79
86
|
|
80
87
|
super(*args, **opts.merge(inputs_transformers:))
|
81
88
|
end
|
@@ -111,6 +118,8 @@ module Foobara
|
|
111
118
|
maximum_call_count: nil,
|
112
119
|
llm_model: nil
|
113
120
|
)
|
121
|
+
set_context_goal(goal)
|
122
|
+
|
114
123
|
if result_type && self.result_type != result_type
|
115
124
|
if self.result_type
|
116
125
|
# :nocov:
|
@@ -135,7 +144,7 @@ module Foobara
|
|
135
144
|
inputs = {
|
136
145
|
goal:,
|
137
146
|
final_result_type: self.result_type,
|
138
|
-
|
147
|
+
context:,
|
139
148
|
agent: self
|
140
149
|
}
|
141
150
|
|
@@ -169,6 +178,10 @@ module Foobara
|
|
169
178
|
inputs[:include_message_to_user_in_result] = include_message_to_user_in_result
|
170
179
|
end
|
171
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
|
+
|
172
185
|
self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
|
173
186
|
|
174
187
|
current_accomplish_goal_command.run.tap do |outcome|
|
@@ -188,9 +201,12 @@ module Foobara
|
|
188
201
|
end
|
189
202
|
end
|
190
203
|
|
191
|
-
def
|
192
|
-
|
193
|
-
|
204
|
+
def set_context_goal(goal)
|
205
|
+
if context
|
206
|
+
context.set_new_goal(goal)
|
207
|
+
else
|
208
|
+
self.context = Context.for(goal)
|
209
|
+
end
|
194
210
|
end
|
195
211
|
|
196
212
|
def mark_mission_accomplished(final_result, message_to_user)
|
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.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miles Georgi
|
@@ -54,6 +54,7 @@ files:
|
|
54
54
|
- src/foobara/agent/connector/set_command_connector_inputs_transformer.rb
|
55
55
|
- src/foobara/agent/describe_command.rb
|
56
56
|
- src/foobara/agent/describe_type.rb
|
57
|
+
- src/foobara/agent/determine_base.rb
|
57
58
|
- src/foobara/agent/determine_inputs_for_next_command.rb
|
58
59
|
- src/foobara/agent/determine_next_command.rb
|
59
60
|
- src/foobara/agent/determine_next_command_name_and_inputs.rb
|