foobara-agent 0.0.9 → 0.0.11

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: c086912a26df48a558f040a3b2e03138210674ec371f528437f08b007ba31783
4
- data.tar.gz: 86315598bc4ca17d59cfc6e2d204561c2b387c0162a19601281f3825a7e68337
3
+ metadata.gz: 0e0f6302247bbcc56ed93f61ec3a5d39e5041029e03b20cadea2d1dd0ebd04b6
4
+ data.tar.gz: baca677e1ea555bcdc908fd2b37dc2d8b016df9888f8f7a69c8cff73774756b2
5
5
  SHA512:
6
- metadata.gz: 2ce3b72dca0e4e111c8a68f61d9c80eeabbfcfd480d858ebeaa92b9e7cc4730fd4278654ce7b3ded885628297aa0eb8684ee73b7213559e9d9f93760a6c06086
7
- data.tar.gz: 28c540d5663172ff9d15925f152357872576d96e275bb278edc5c6661c47dfbe9fc0832fec9268fe1b215dfaeb5d43d30eef422bc572dc4e9023e82cd77c3a79
6
+ metadata.gz: 350feb6d9058009889a342a5dcc4f44befed8a25be71ebc7673bf75b7ceb1f95adcec3a52d2eb53ba9301d0ef2312919a4496a53f2146423dd36c17b122856af
7
+ data.tar.gz: a91ec6608bc3001f06d3c17562321d2c1a52a537372347593293d57d9d6312126f5116853c8b32b649d7c65a050bbb67bd03b2dbc50247cdc7260013b6d8f3ec
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.0.11] - 2025-07-01
2
+
3
+ - Fix some bugs with result type/value mismatches
4
+ - Eliminate ability to select command and inputs separately
5
+
6
+ ## [0.0.10] - 2025-06-30
7
+
8
+ - Allow more retries
9
+ - Eliminate ability to select command and inputs separately
10
+
1
11
  ## [0.0.9] - 2025-06-28
2
12
 
3
13
  - Relocate some common Determine* behavior into a DetermineBase
data/lib/foobara/agent.rb CHANGED
@@ -11,7 +11,6 @@ module Foobara
11
11
  class << self
12
12
  def reset_all
13
13
  [
14
- DetermineInputsForNextCommand,
15
14
  NotifyUserThatCurrentGoalHasBeenAccomplished
16
15
  ].each do |command_class|
17
16
  command_class.clear_subclass_cache
@@ -23,4 +22,4 @@ module Foobara
23
22
  end
24
23
 
25
24
  Foobara::Util.require_directory "#{__dir__}/../../src"
26
- Foobara::Monorepo.project "agent", project_path: "#{__dir__}/../../"
25
+ Foobara.project "agent", project_path: "#{__dir__}/../../"
@@ -29,12 +29,6 @@ module Foobara
29
29
  one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
30
30
  default: "claude-3-7-sonnet-20250219",
31
31
  description: "The model to use for the LLM"
32
- choose_next_command_and_next_inputs_separately :boolean,
33
- default: false,
34
- description:
35
- "By default, asks for next command and inputs together. " \
36
- "You can experiment with getting the separately " \
37
- "with this flag if you wish."
38
32
  max_llm_calls_per_minute :integer, :allow_nil
39
33
  end
40
34
 
@@ -56,15 +50,10 @@ module Foobara
56
50
 
57
51
  throttle_llm_calls_if_necessary
58
52
 
59
- if choose_next_command_and_next_inputs_separately?
60
- determine_next_command_then_inputs_separately
61
- else
62
- determine_next_command_and_inputs
63
- end
64
-
53
+ determine_next_command_and_inputs
65
54
  run_next_command
55
+
66
56
  log_last_command_outcome
67
- compact_command_log
68
57
  end
69
58
 
70
59
  if given_up
@@ -138,7 +127,24 @@ module Foobara
138
127
  # :nocov:
139
128
  end
140
129
 
141
- def determine_next_command_and_inputs(retries = 2)
130
+ def determine_next_command_and_inputs(retries = 3, error_outcome = nil)
131
+ if retries == 0
132
+ # TODO: test this path by irreparably breaking the needed commands
133
+ # :nocov:
134
+ self.next_command_name = GiveUp.full_command_name
135
+ self.next_command_inputs = {
136
+ message_to_user: "While trying to choose the next command and inputs, " \
137
+ "I've ran into an error several times that I couldn't figure out how to get past. " \
138
+ "The last error looked like this:\n#{error_outcome.errors_hash}"
139
+ }
140
+ self.next_command_raw_inputs = next_command_inputs
141
+
142
+ return
143
+ # :nocov:
144
+ end
145
+
146
+ compact_command_log
147
+
142
148
  inputs_for_determine = {
143
149
  context:,
144
150
  llm_model:
@@ -156,7 +162,7 @@ module Foobara
156
162
  end
157
163
 
158
164
  if outcome.success?
159
- self.next_command_name = outcome.result[:command_name]
165
+ self.next_command_name = outcome.result[:command]
160
166
  self.next_command_inputs = outcome.result[:inputs]
161
167
  self.next_command_raw_inputs = next_command_inputs
162
168
 
@@ -176,7 +182,8 @@ module Foobara
176
182
  )
177
183
 
178
184
  simulate_describe_command
179
- determine_next_command_inputs
185
+
186
+ determine_next_command_and_inputs(retries - 1, outcome)
180
187
  end
181
188
  else
182
189
  self.next_command_inputs = {}
@@ -190,11 +197,7 @@ module Foobara
190
197
  result: nil
191
198
  )
192
199
 
193
- if retries > 0
194
- determine_next_command_and_inputs(retries - 1)
195
- else
196
- determine_next_command_then_inputs_separately
197
- end
200
+ determine_next_command_and_inputs(retries - 1, outcome)
198
201
  end
199
202
  else
200
203
  log_command_outcome(
@@ -204,23 +207,7 @@ module Foobara
204
207
  result: outcome.result || determine_command.raw_result
205
208
  )
206
209
 
207
- if retries > 0
208
- determine_next_command_and_inputs(retries - 1)
209
- else
210
- determine_next_command_then_inputs_separately
211
- end
212
- end
213
- end
214
-
215
- def determine_next_command_then_inputs_separately
216
- determine_next_command_name
217
-
218
- if command_described?
219
- fetch_next_command_class
220
- determine_next_command_inputs
221
- else
222
- choose_describe_command_instead
223
- fetch_next_command_class
210
+ determine_next_command_and_inputs(retries - 1, outcome)
224
211
  end
225
212
  end
226
213
 
@@ -259,74 +246,6 @@ module Foobara
259
246
  @command_name_type ||= Agent.foobara_type_from_declaration(:string, one_of: all_command_classes)
260
247
  end
261
248
 
262
- def determine_next_command_name(retries = 2)
263
- self.next_command_name = if delayed_command_name
264
- name = delayed_command_name
265
- self.delayed_command_name = nil
266
- name
267
- else
268
- inputs = { context: }
269
- if llm_model
270
- inputs[:llm_model] = llm_model
271
- end
272
-
273
- command = DetermineNextCommand.new(inputs)
274
- outcome = begin
275
- record_llm_call_timestamp
276
- command.run
277
- rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
278
- # :nocov:
279
- Outcome.errors(e.errors)
280
- # :nocov:
281
- end
282
-
283
- if outcome.success?
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:
291
- log_command_outcome(
292
- command:,
293
- inputs: command.inputs.except(:context),
294
- outcome:,
295
- result: outcome.result || command.raw_result
296
- )
297
-
298
- if retries > 0
299
- return determine_next_command_name(retries - 1)
300
- end
301
- # :nocov:
302
- end
303
- else
304
- # TODO: either figure out a way to hit this path in the test suite or delete it
305
- # :nocov:
306
- log_command_outcome(
307
- command:,
308
- inputs: command.inputs&.except(:context),
309
- outcome:,
310
- result: outcome.result || command.raw_result
311
- )
312
-
313
- if retries > 0
314
- return determine_next_command_name(retries - 1)
315
- end
316
- # :nocov:
317
- end
318
-
319
- outcome.raise!
320
- outcome.result
321
- end
322
- end
323
-
324
- def choose_describe_command_instead
325
- self.delayed_command_name = next_command_name
326
- self.next_command_inputs = { command_name: next_command_name }
327
- self.next_command_name = DescribeCommand.full_command_name
328
- end
329
-
330
249
  def all_command_classes
331
250
  @all_command_classes ||= run_subcommand!(ListCommands, command_connector: agent).values.flatten
332
251
  end
@@ -335,58 +254,11 @@ module Foobara
335
254
  self.next_command_class = agent.transformed_command_from_name(next_command_name)
336
255
  end
337
256
 
338
- def determine_next_command_inputs(retries = 2)
339
- self.next_command_inputs = if next_command_has_inputs?
340
- command_class = command_class_for_determine_inputs_for_next_command
341
-
342
- inputs = { context: }
343
- if llm_model
344
- inputs[:llm_model] = llm_model
345
- end
346
-
347
- command = command_class.new(inputs)
348
- outcome = begin
349
- record_llm_call_timestamp
350
- command.run
351
- rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
352
- # :nocov:
353
- Outcome.errors(e.errors)
354
- # :nocov:
355
- end
356
-
357
- unless outcome.success?
358
- # TODO: either figure out a way to hit this path in the test suite or delete it
359
- # :nocov:
360
- log_command_outcome(
361
- command_name: next_command_name,
362
- inputs: command.raw_result,
363
- outcome:
364
- )
365
-
366
- simulate_describe_command
367
-
368
- if retries > 0
369
- return determine_next_command_inputs(retries - 1)
370
- end
371
- # :nocov:
372
- end
373
-
374
- outcome.raise!
375
- outcome.result
376
- end
377
- end
378
-
379
257
  def next_command_has_inputs?
380
258
  type = next_command_class.inputs_type
381
259
  type && !empty_attributes?(type)
382
260
  end
383
261
 
384
- def command_class_for_determine_inputs_for_next_command
385
- DetermineInputsForNextCommand.for(
386
- command_class: next_command_class, agent_id: agent_name
387
- )
388
- end
389
-
390
262
  def run_next_command
391
263
  if verbose?
392
264
  args = if next_command_inputs.nil? || next_command_inputs.empty?
@@ -402,6 +274,7 @@ module Foobara
402
274
  # :nocov:
403
275
  end
404
276
  end
277
+
405
278
  (io_out || $stdout).puts "#{next_command_name}.run#{args}"
406
279
  end
407
280
 
@@ -431,8 +304,8 @@ module Foobara
431
304
  def compact_command_log
432
305
  # Rules:
433
306
  # Delete errors for any command that has succeeded since
434
- # Delete all but the last DescribeCommand call
435
- describe_command_call_indexes = []
307
+ # Delete all but the last DescribeCommand call for a given
308
+ describe_command_call_indexes = {}
436
309
  commands = {}
437
310
 
438
311
  describe_command_name = DescribeCommand.full_command_name
@@ -440,7 +313,12 @@ module Foobara
440
313
  command_name = command_log_entry.command_name
441
314
 
442
315
  if command_name == describe_command_name
443
- describe_command_call_indexes << index
316
+ described_command = command_log_entry.inputs[:command_name]
317
+
318
+ if command_log_entry.outcome[:success]
319
+ describe_command_call_indexes[described_command] ||= []
320
+ describe_command_call_indexes[described_command] << index
321
+ end
444
322
  end
445
323
 
446
324
  commands[command_name] ||= [[], []]
@@ -448,7 +326,11 @@ module Foobara
448
326
  commands[command_name][bucket_index] << index
449
327
  end
450
328
 
451
- indexes_to_delete = describe_command_call_indexes[0..-2]
329
+ indexes_to_delete = []
330
+
331
+ describe_command_call_indexes.each_value do |indexes|
332
+ indexes_to_delete += indexes[0..-2]
333
+ end
452
334
 
453
335
  commands.each_value do |(success_indexes, failure_indexes)|
454
336
  last_success = success_indexes.last
@@ -548,10 +430,6 @@ module Foobara
548
430
  }
549
431
  end
550
432
 
551
- def command_described?
552
- described_commands.include?(next_command_name)
553
- end
554
-
555
433
  def described_commands
556
434
  @described_commands ||= Set.new
557
435
  end
@@ -560,14 +438,6 @@ module Foobara
560
438
  type.extends_type?(BuiltinTypes[:attributes]) && type.element_types.empty?
561
439
  end
562
440
 
563
- def choose_next_command_and_next_inputs_separately?
564
- choose_next_command_and_next_inputs_separately
565
- end
566
-
567
- def agent_name
568
- agent.agent_name
569
- end
570
-
571
441
  def verbose?
572
442
  verbose
573
443
  end
@@ -596,7 +466,7 @@ module Foobara
596
466
  first_to_expire = calls.first
597
467
 
598
468
  if first_to_expire
599
- (first_to_expire + SECONDS_PER_MINUTE) - Time.now
469
+ [0, (first_to_expire + SECONDS_PER_MINUTE) - Time.now].max
600
470
  else
601
471
  # TODO: figure out how to test this code path
602
472
  # :nocov:
@@ -13,7 +13,9 @@ module Foobara
13
13
 
14
14
  def cached_subclass(key)
15
15
  if subclass_cache.key?(key)
16
+ # :nocov:
16
17
  subclass_cache[key]
18
+ # :nocov:
17
19
  else
18
20
  subclass_cache[key] = yield
19
21
  end
@@ -9,7 +9,7 @@ module Foobara
9
9
  "NotifyUserThatCurrentGoalHasBeenAccomplished command."
10
10
 
11
11
  result do
12
- command_name :string, :required
12
+ command :string, :required
13
13
  inputs :attributes, :allow_nil
14
14
  end
15
15
  end
@@ -4,7 +4,7 @@ module Foobara
4
4
  extend Concerns::SubclassCacheable
5
5
 
6
6
  class << self
7
- attr_accessor :command_class
7
+ attr_accessor :command_class, :returns_message_to_user, :returns_result_data, :result_is_attributes
8
8
 
9
9
  def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true)
10
10
  agent_id ||= "Anon#{SecureRandom.hex(2)}"
@@ -19,24 +19,36 @@ module Foobara
19
19
  "The user might issue a new goal."
20
20
 
21
21
  if result_type
22
+ klass.returns_result_data = true
23
+
24
+ # TODO: fix this... agent backed command sets these via its own result type.
25
+ # check if message_to_user is already here and also search/fix result_data to be result for consistency.
22
26
  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
+ klass.returns_message_to_user = true
27
28
 
28
29
  klass.result do
29
30
  result result_type, :required
30
31
  message_to_user :string, :required, "Message to the user about what was done"
31
32
  end
32
33
 
34
+ klass.add_inputs klass.result_type
35
+
33
36
  klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
34
37
  "result formatted according to the " \
35
38
  "result schema and a message to the user. " \
36
39
  "The user might issue a new goal."
37
40
  else
38
- klass.add_inputs do
39
- result result_type, :required
41
+ unless result_type.is_a?(Types::Type)
42
+ result_type = Domain.current.foobara_type_from_declaration(result_type)
43
+ end
44
+
45
+ if result_type.extends?(BuiltinTypes[:attributes])
46
+ klass.result_is_attributes = true
47
+ klass.add_inputs result_type
48
+ else
49
+ klass.add_inputs do
50
+ result result_type, :required
51
+ end
40
52
  end
41
53
 
42
54
  klass.result result_type
@@ -47,6 +59,8 @@ module Foobara
47
59
  "The user might issue a new goal."
48
60
  end
49
61
  elsif include_message_to_user_in_result
62
+ klass.returns_message_to_user = true
63
+
50
64
  klass.add_inputs do
51
65
  message_to_user :string, :required, "Message to the user about what was done"
52
66
  end
@@ -79,24 +93,46 @@ module Foobara
79
93
  end
80
94
 
81
95
  def execute
96
+ build_result
82
97
  mark_mission_accomplished
83
98
 
84
- parsed_result
99
+ built_result
85
100
  end
86
101
 
102
+ attr_accessor :built_result
103
+
87
104
  def mark_mission_accomplished
88
- command_connector.mark_mission_accomplished(inputs[:result], inputs[:message_to_user])
105
+ result, message_to_user = if returns_message_to_user?
106
+ [built_result[:result], built_result[:message_to_user]]
107
+ elsif returns_result_data?
108
+ [built_result, nil]
109
+ end
110
+
111
+ command_connector.mark_mission_accomplished(result, message_to_user)
89
112
  end
90
113
 
91
- def parsed_result
92
- inputs_type = self.class.inputs_type
93
- element_types = inputs_type.element_types
114
+ def build_result
115
+ self.built_result = if returns_message_to_user?
116
+ inputs.slice(:result, :message_to_user)
117
+ elsif returns_result_data?
118
+ if result_is_attributes?
119
+ inputs.slice(*self.class.result_type.element_types.keys)
120
+ else
121
+ inputs[:result]
122
+ end
123
+ end
124
+ end
94
125
 
95
- if element_types.key?(:message_to_user)
96
- inputs.slice(:result, :message_to_user)
97
- elsif element_types.key?(:result)
98
- inputs[:result]
99
- end
126
+ def returns_message_to_user?
127
+ self.class.returns_message_to_user
128
+ end
129
+
130
+ def returns_result_data?
131
+ self.class.returns_result_data
132
+ end
133
+
134
+ def result_is_attributes?
135
+ self.class.result_is_attributes
100
136
  end
101
137
  end
102
138
  end
data/src/foobara/agent.rb CHANGED
@@ -41,7 +41,7 @@ module Foobara
41
41
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
42
42
  self.context = context
43
43
  self.agent_name = agent_name || "Anon#{SecureRandom.hex(2)}"
44
- self.llm_model = llm_model
44
+ self.llm_model = llm_model || "claude-opus-4-20250514"
45
45
  self.result_type = result_type
46
46
  self.include_message_to_user_in_result = include_message_to_user_in_result
47
47
  self.verbose = verbose
@@ -114,7 +114,6 @@ module Foobara
114
114
  def accomplish_goal(
115
115
  goal,
116
116
  result_type: nil,
117
- choose_next_command_and_next_inputs_separately: nil,
118
117
  maximum_call_count: nil,
119
118
  llm_model: nil
120
119
  )
@@ -154,10 +153,6 @@ module Foobara
154
153
  inputs[:llm_model] = llm_model
155
154
  end
156
155
 
157
- unless choose_next_command_and_next_inputs_separately.nil?
158
- inputs[:choose_next_command_and_next_inputs_separately] = choose_next_command_and_next_inputs_separately
159
- end
160
-
161
156
  unless maximum_call_count.nil?
162
157
  inputs[:maximum_command_calls] = maximum_call_count
163
158
  end
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.9
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -55,8 +55,6 @@ files:
55
55
  - src/foobara/agent/describe_command.rb
56
56
  - src/foobara/agent/describe_type.rb
57
57
  - src/foobara/agent/determine_base.rb
58
- - src/foobara/agent/determine_inputs_for_next_command.rb
59
- - src/foobara/agent/determine_next_command.rb
60
58
  - src/foobara/agent/determine_next_command_name_and_inputs.rb
61
59
  - src/foobara/agent/give_up.rb
62
60
  - src/foobara/agent/list_commands.rb
@@ -1,33 +0,0 @@
1
- module Foobara
2
- class Agent < CommandConnector
3
- class DetermineInputsForNextCommand < DetermineBase
4
- extend Concerns::SubclassCacheable
5
-
6
- class << self
7
- def for(command_class:, agent_id:)
8
- cached_subclass([command_class.full_command_name, agent_id]) do
9
- command_short_name = Util.non_full_name(command_class.command_name)
10
- class_name = "Foobara::Agent::#{agent_id}::DetermineInputsForNext#{command_short_name}Command"
11
- klass = Util.make_class_p(class_name, self)
12
-
13
- klass.description "Returns the inputs for " \
14
- "the next #{command_class.full_command_name} command to run."
15
-
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:
20
- end
21
-
22
- transformer = CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer.new(
23
- to: command_class.inputs_type
24
- )
25
- klass.result transformer.from_type
26
-
27
- klass
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,13 +0,0 @@
1
- module Foobara
2
- class Agent < CommandConnector
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"
11
- end
12
- end
13
- end