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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 121474f4e61c1b3a5eebc883794bef41159d48ecc05eba44165bd8d79111c426
4
- data.tar.gz: 50bc4c4aea5bd447f17cf39712b7a86295e4f45708602398f72a61af2f221881
3
+ metadata.gz: 12e8facdeb95a000d81ec0ba8a133affa1ea66e470115e978bdc81f05bc51855
4
+ data.tar.gz: 294064df166d152c037db13263863b8ccfe32f064a78e77f6f5fe59af0be3829
5
5
  SHA512:
6
- metadata.gz: e0cbf20f7fc287bd4a3be5caed0572de59475493ae4bed80d755b5284ead25fa697b455959ca323891bb20f0bfb57a5a01f1e62f335faacc6c99c220821920f3
7
- data.tar.gz: cf85ac42b6ab4a9f8efddd35e533afd32f664b9a9ec8bba17f77abf424a5499e64ba6ad576b54bb24c03dadb5e6bc1c2fe54a502bd505f30caaeeb15e002d655
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
@@ -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,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
- existing_command_connector CommandConnector, :allow_nil,
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, :required, "Message to the user about successfully accomplishing the goal"
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
- if command_connector_passed_in?
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
- def agent_commands_connected?
92
- command_connector.agent_commands_connected?
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
- end
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
- if outcome.success?
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
- command: determine_command,
217
- inputs: determine_command.inputs.except(:context),
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
- command: determine_command,
230
- inputs: determine_command.inputs&.except(:context),
173
+ command_name: next_command_name,
174
+ inputs: next_command_inputs,
231
175
  outcome:,
232
- result: outcome.result || determine_command.raw_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 = command_class.new(inputs)
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
- if log_successful_determine_command_and_inputs_outcomes?
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 = command_connector.transformed_command_from_name(next_command_name)
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
- if outcome.success?
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
- command:,
389
- inputs: command.inputs.except(:context),
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
- (io_out || $stdout).puts "Running #{next_command_name} with #{next_command_inputs}"
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 = command_connector.run(
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
- if command_outcome.success?
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.inputs
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
- klass.result command_class.inputs_type
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
- extend Concerns::SubclassCacheable
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
- 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
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
- 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."
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:, agent_id:)
10
- cached_subclass([result_type, agent_id]) do
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
- add_inputs do
26
- result_data result_type, :required
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
- # TODO: test this code path
62
+ # This should be unreachable actually
40
63
  # :nocov:
41
- klass.description "Notifies the user that the current goal has been accomplished and, if relevant, " \
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
- # TODO: Are we still not able to uses classes as foobara types??
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
- data = if result_type
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
- h = { message_to_user: }
92
+ inputs_type = self.class.inputs_type
93
+ element_types = inputs_type.element_types
80
94
 
81
- if inputs[:result_data] && result_type
82
- h[:result_data] = result_data
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
- current_accomplish_goal_command: nil,
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 if 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.current_accomplish_goal_command = current_accomplish_goal_command
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
- unless opts.key?(:default_serializers)
49
- opts = opts.merge(default_serializers: [
50
- Foobara::CommandConnectors::Serializers::ErrorsSerializer,
51
- Foobara::CommandConnectors::Serializers::AggregateSerializer
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
- opts = opts.merge(aggregate_entities: true)
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
- existing_command_connector: self
148
+ agent: self
134
149
  }
135
150
 
136
- if agent_name
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
- state_machine.perform_transition!(:goal_accomplished)
169
- else
170
- state_machine.perform_transition!(:goal_errored)
171
- end
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(final_result_type: nil, agent_name: nil)
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 final_result_type
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: final_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.6
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