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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 968399c2d338c8f069712d0ad9437b928fe48bc7356e88fd86b9ff6fb7ae235f
4
- data.tar.gz: 8483b856faf2f559985550cc631764121797f88963c0baf428b7f16646bd8b59
3
+ metadata.gz: c086912a26df48a558f040a3b2e03138210674ec371f528437f08b007ba31783
4
+ data.tar.gz: 86315598bc4ca17d59cfc6e2d204561c2b387c0162a19601281f3825a7e68337
5
5
  SHA512:
6
- metadata.gz: 79be2912847cb407d1ad65166313c8b799b981a2b0da6f8c8e3f8132049c1eb67ec56a9307fdf887f8e659299883f0da4517762abc885fdf8b69949f869f54de
7
- data.tar.gz: 169c02a8070d5feb4d307b41ff757294ca5a2e3b810ac0766cece386a357db2cf85075ae9ecb1f5ee2e0b1ec92c855eb202ee4166102a88341b8e5b4268eeb92
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
@@ -12,7 +12,6 @@ module Foobara
12
12
  def reset_all
13
13
  [
14
14
  DetermineInputsForNextCommand,
15
- DetermineNextCommand,
16
15
  NotifyUserThatCurrentGoalHasBeenAccomplished
17
16
  ].each do |command_class|
18
17
  command_class.clear_subclass_cache
@@ -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
- current_context Context, :allow_nil, "The current context of the agent"
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
- build_initial_context_if_necessary
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 :context, :next_command_name, :next_command_inputs, :mission_accomplished, :given_up,
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
- if outcome.success?
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
- command: determine_command,
154
- inputs: determine_command.inputs.except(:context),
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
- command: determine_command,
167
- inputs: determine_command.inputs&.except(:context),
187
+ command_name: next_command_name,
188
+ inputs: next_command_inputs,
168
189
  outcome:,
169
- result: outcome.result || determine_command.raw_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
- command_class = DetermineNextCommand.for(
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 = command_class.new(inputs)
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
- if log_successful_determine_command_and_inputs_outcomes?
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.except(:context),
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 = { goal:, context: }
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
- if outcome.success?
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
- command:,
326
- inputs: command.inputs.except(:context),
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
- (io_out || $stdout).puts "Running #{next_command_name} with #{next_command_inputs}"
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
- if command_outcome.success?
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.inputs
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 < Foobara::LlmBackedCommand
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 "Accepts a goal and context of the work so far and returns the inputs for " \
16
- "the next #{command_short_name} command to run to make progress towards " \
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
- klass.inputs do
20
- goal :string, :required, "The current (possibly already accomplished) goal"
21
- context Context, :required, "Context of the progress towards the goal so far"
22
- llm_model :string,
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
- if command_class.inputs_type
29
- klass.result command_class.inputs_type
30
- end
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 < Foobara::LlmBackedCommand
6
- extend Concerns::SubclassCacheable
7
-
8
- class << self
9
- # Allows us to give a more meaningful result type
10
- def for(command_class_names:, agent_id:)
11
- cached_subclass(agent_id) do
12
- command_name = "Foobara::Agent::#{agent_id}::DetermineNextCommand"
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
- require "foobara/llm_backed_command"
1
+ require_relative "determine_base"
2
2
 
3
3
  module Foobara
4
4
  class Agent < CommandConnector
5
- class DetermineNextCommandNameAndInputs < Foobara::LlmBackedCommand
6
- description "Accepts the current goal, which might already be accomplished, and context of the work " \
7
- "so far and returns the inputs for " \
8
- "the next command to run to make progress towards " \
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
- unless opts.key?(:default_serializers)
50
- opts = opts.merge(default_serializers: [
51
- Foobara::CommandConnectors::Serializers::ErrorsSerializer,
52
- Foobara::CommandConnectors::Serializers::AggregateSerializer
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
- opts = opts.merge(aggregate_entities: true)
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
- current_context: context,
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 build_initial_context
192
- # TODO: shouldn't have to pass command_log here since it has a default, debug that
193
- self.context ||= Context.new(command_log: [])
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.7
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