foobara-agent 0.0.10 → 0.0.13

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: 4c8f095d06fb173bc816e83f19755cfe246f15bf2b8ef1f8f91bce08e5960e74
4
- data.tar.gz: 15b48a59c43c909070f68bbc58c68ce02635d019f9d3af68f1c3aa4264cd4903
3
+ metadata.gz: dc4b3804b5930b121fdc679ecf203b9e8b4d11122d24da967cfbfd10a1e6562d
4
+ data.tar.gz: '0739391b9b59d453783756a8efed38ec3d559ce7636873776ee71a14027059b5'
5
5
  SHA512:
6
- metadata.gz: f558c74920f9759a98b2605fe2dcbbc52678ff5d90a0977182a75d0949cfd400b07d3d17d4dbfe162b37a3f463c3c55b4e25dee6382f2daeb3256b5df9615f73
7
- data.tar.gz: f0588e94676e579c1fd5d7b3b603452d0c283b2b14217511b74a8d341d3126ff42cf7d0cb1cc881048d356525c7b9250f73c1a7631dae556a589f23b8627e1f8
6
+ metadata.gz: 0ff04dba39a3edae5a80a98234cd7708c2f90d30bded9f12a0e2b665ea640c302e1ed14c607d61533daa5a0b3d7712684d92de740a3f3e085a1704286f95b837
7
+ data.tar.gz: 252b0f1a1c298bd984a856f4e63e57808b63cace046d58cf82f8c55f391c6d21fb3d98b8dda869582aa995eb6438a2fe998bae3952c5f6791eca00d25c5d181c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.0.13] - 2025-07-06
2
+
3
+ - Eliminate NotifyUser... result_type to save tokens/improve accuracy
4
+ - Choose DescribeCommand for commands with inputs that haven't been described
5
+ - Log failure output when trying to determine commands/inputs
6
+
7
+ ## [0.0.12] - 2025-07-05
8
+
9
+ - Choose DescribeCommand for commands that have inputs and haven't been described
10
+ - Log determine command/input failures when verbose
11
+
12
+ ## [0.0.11] - 2025-07-01
13
+
14
+ - Fix some bugs with result type/value mismatches
15
+ - Eliminate ability to select command and inputs separately
16
+
1
17
  ## [0.0.10] - 2025-06-30
2
18
 
3
19
  - Allow more retries
data/lib/foobara/agent.rb CHANGED
@@ -22,4 +22,4 @@ module Foobara
22
22
  end
23
23
 
24
24
  Foobara::Util.require_directory "#{__dir__}/../../src"
25
- Foobara::Monorepo.project "agent", project_path: "#{__dir__}/../../"
25
+ Foobara.project "agent", project_path: "#{__dir__}/../../"
@@ -30,6 +30,7 @@ module Foobara
30
30
  default: "claude-3-7-sonnet-20250219",
31
31
  description: "The model to use for the LLM"
32
32
  max_llm_calls_per_minute :integer, :allow_nil
33
+ result_entity_depth :symbol, :allow_nil, one_of: [:atom, :aggregate]
33
34
  end
34
35
 
35
36
  result do
@@ -51,9 +52,8 @@ module Foobara
51
52
  throttle_llm_calls_if_necessary
52
53
 
53
54
  determine_next_command_and_inputs
54
- run_next_command
55
55
 
56
- log_last_command_outcome
56
+ run_next_command
57
57
  end
58
58
 
59
59
  if given_up
@@ -75,7 +75,6 @@ module Foobara
75
75
  fetch_next_command_class
76
76
 
77
77
  run_next_command
78
- log_last_command_outcome
79
78
  end
80
79
 
81
80
  def simulate_describe_list_commands_command
@@ -85,7 +84,6 @@ module Foobara
85
84
  fetch_next_command_class
86
85
 
87
86
  run_next_command
88
- log_last_command_outcome
89
87
  end
90
88
 
91
89
  def simulate_describe_command(command_name = next_command_name)
@@ -100,7 +98,6 @@ module Foobara
100
98
  fetch_next_command_class
101
99
 
102
100
  run_next_command
103
- log_last_command_outcome
104
101
 
105
102
  self.next_command_name = old_next_command_name
106
103
  self.next_command_inputs = old_next_command_inputs
@@ -114,7 +111,7 @@ module Foobara
114
111
  return if context.command_log.size > 1
115
112
 
116
113
  ListCommands.run!(command_connector: agent)[:user_provided_commands].each do |full_command_name|
117
- next if described_commands.include?(full_command_name)
114
+ next if command_described?(full_command_name)
118
115
 
119
116
  self.next_command_name = DescribeCommand.full_command_name
120
117
  self.next_command_inputs = { command_name: full_command_name }
@@ -122,7 +119,6 @@ module Foobara
122
119
  fetch_next_command_class
123
120
 
124
121
  run_next_command
125
- log_last_command_outcome
126
122
  end
127
123
  # :nocov:
128
124
  end
@@ -145,11 +141,7 @@ module Foobara
145
141
 
146
142
  compact_command_log
147
143
 
148
- inputs_for_determine = {
149
- context:,
150
- llm_model:
151
- }
152
-
144
+ inputs_for_determine = { context:, llm_model: }
153
145
  determine_command = DetermineNextCommandNameAndInputs.new(inputs_for_determine)
154
146
 
155
147
  outcome = begin
@@ -171,10 +163,17 @@ module Foobara
171
163
  if outcome.success?
172
164
  fetch_next_command_class
173
165
 
166
+ if need_to_describe_next_command?
167
+ simulate_describe_command
168
+ return determine_next_command_and_inputs(retries, outcome)
169
+ end
170
+
174
171
  if next_command_has_inputs?
175
172
  outcome = validate_next_command_inputs
176
173
 
177
174
  unless outcome.success?
175
+ # TODO: test this path
176
+ # :nocov:
178
177
  log_command_outcome(
179
178
  command_name: next_command_name,
180
179
  inputs: next_command_inputs,
@@ -184,6 +183,7 @@ module Foobara
184
183
  simulate_describe_command
185
184
 
186
185
  determine_next_command_and_inputs(retries - 1, outcome)
186
+ # :nocov:
187
187
  end
188
188
  else
189
189
  self.next_command_inputs = {}
@@ -260,23 +260,7 @@ module Foobara
260
260
  end
261
261
 
262
262
  def run_next_command
263
- if verbose?
264
- args = if next_command_inputs.nil? || next_command_inputs.empty?
265
- ""
266
- else
267
- s = next_command_inputs.to_s
268
-
269
- if s =~ /\A\{(.*)}\z/
270
- "(#{::Regexp.last_match(1)})"
271
- else
272
- # :nocov:
273
- raise "Unexpected next_command_inputs: #{next_command_inputs}"
274
- # :nocov:
275
- end
276
- end
277
-
278
- (io_out || $stdout).puts "#{next_command_name}.run#{args}"
279
- end
263
+ log_command_code(command_name: next_command_name, inputs: next_command_inputs)
280
264
 
281
265
  self.command_response = agent.run(
282
266
  full_command_name: next_command_name,
@@ -286,19 +270,7 @@ module Foobara
286
270
 
287
271
  self.command_outcome = command_response.outcome
288
272
 
289
- if verbose?
290
- unless command_outcome.success?
291
- # :nocov:
292
- (io_err || $stderr).puts(
293
- "Command #{command_response.command.class.full_command_name} failed #{command_outcome.errors_hash}"
294
- )
295
- # :nocov:
296
- end
297
- end
298
- end
299
-
300
- def log_last_command_outcome
301
- log_command_outcome(command: command_response.command)
273
+ log_command_outcome(command: command_response.command, log_command_code: false)
302
274
  end
303
275
 
304
276
  def compact_command_log
@@ -337,13 +309,14 @@ module Foobara
337
309
  next unless last_success
338
310
 
339
311
  failure_indexes.each do |failure_index|
312
+ # TODO: test this path
313
+ # :nocov:
340
314
  if failure_index < last_success
341
315
  indexes_to_delete << failure_index
342
316
  else
343
- # :nocov:
344
317
  break
345
- # :nocov:
346
318
  end
319
+ # :nocov:
347
320
  end
348
321
  end
349
322
 
@@ -377,7 +350,12 @@ module Foobara
377
350
  end
378
351
  end
379
352
 
380
- def log_command_outcome(command: nil, command_name: nil, inputs: nil, outcome: nil, result: nil)
353
+ def log_command_outcome(command: nil,
354
+ command_name: nil,
355
+ inputs: nil,
356
+ outcome: nil,
357
+ result: nil,
358
+ log_command_code: true)
381
359
  if command
382
360
  command_name ||= command.class.full_command_name
383
361
  inputs ||= command.raw_inputs
@@ -405,6 +383,43 @@ module Foobara
405
383
  )
406
384
 
407
385
  context.command_log << log_entry
386
+
387
+ if verbose?
388
+ if log_command_code
389
+ # TODO: test this code path hmmm
390
+ # :nocov:
391
+ self.log_command_code(command_name:, inputs:)
392
+ # :nocov:
393
+ end
394
+
395
+ unless log_entry.success?
396
+ # :nocov:
397
+ (io_err || $stderr).puts(
398
+ "Command #{log_entry.command_name} failed:\n#{log_entry.errors_hash}"
399
+ )
400
+ # :nocov:
401
+ end
402
+ end
403
+ end
404
+
405
+ def log_command_code(command_name:, inputs:)
406
+ if verbose?
407
+ args = if next_command_inputs.nil? || next_command_inputs.empty?
408
+ ""
409
+ else
410
+ s = next_command_inputs.to_s
411
+
412
+ if s =~ /\A\{(.*)}\z/
413
+ "(#{::Regexp.last_match(1)})"
414
+ else
415
+ # :nocov:
416
+ raise "Unexpected next_command_inputs: #{next_command_inputs}"
417
+ # :nocov:
418
+ end
419
+ end
420
+
421
+ (io_out || $stdout).puts "#{next_command_name}.run#{args}"
422
+ end
408
423
  end
409
424
 
410
425
  # TODO: these are awkwardly called from outside. Come up with a better solution.
@@ -442,6 +457,20 @@ module Foobara
442
457
  verbose
443
458
  end
444
459
 
460
+ def need_to_describe_next_command?
461
+ return false if command_described?(next_command_name)
462
+ return false if next_command_name == DescribeCommand.full_command_name
463
+ return true if next_command_has_inputs?
464
+
465
+ # check if inputs were unexpectedly given for a command that doesn't need them,
466
+ # in which case we should describe it
467
+ next_command_inputs && !next_command_inputs.empty?
468
+ end
469
+
470
+ def command_described?(command_name)
471
+ described_commands.include?(command_name)
472
+ end
473
+
445
474
  def llm_call_timestamps
446
475
  @llm_call_timestamps ||= []
447
476
  end
@@ -1,15 +1,18 @@
1
1
  module Foobara
2
2
  class Agent < CommandConnector
3
+ # NOTE: we intentionally don't use a result type here
4
+ # to reduce tokens the LLM has to deal with when this command is described
3
5
  class NotifyUserThatCurrentGoalHasBeenAccomplished < Foobara::Command
4
6
  extend Concerns::SubclassCacheable
5
7
 
6
8
  class << self
7
- attr_accessor :command_class
9
+ attr_accessor :command_class, :returns_message_to_user, :returns_result_data, :result_is_attributes,
10
+ :built_result_type
8
11
 
9
- def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true)
12
+ def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true, result_entity_depth: nil)
10
13
  agent_id ||= "Anon#{SecureRandom.hex(2)}"
11
14
 
12
- cached_subclass([result_type, agent_id, include_message_to_user_in_result]) do
15
+ cached_subclass([agent_id]) do
13
16
  command_name = "Foobara::Agent::#{agent_id}::NotifyUserThatCurrentGoalHasBeenAccomplished"
14
17
  klass = Util.make_class_p(command_name, self)
15
18
 
@@ -19,41 +22,50 @@ module Foobara
19
22
  "The user might issue a new goal."
20
23
 
21
24
  if result_type
25
+ klass.returns_result_data = true
26
+ unless result_type.is_a?(Types::Type)
27
+ result_type = Domain.current.foobara_type_from_declaration(result_type)
28
+ end
29
+
30
+ domain = result_type.created_in_namespace.foobara_domain
31
+
22
32
  # TODO: fix this... agent backed command sets these via its own result type.
23
33
  # check if message_to_user is already here and also search/fix result_data to be result for consistency.
24
34
  if include_message_to_user_in_result
25
- klass.add_inputs do
26
- result result_type, :required
27
- message_to_user :string, :required, "Message to the user about what was done"
28
- end
35
+ klass.returns_message_to_user = true
29
36
 
30
- klass.result do
37
+ klass.built_result_type = domain.foobara_type_from_declaration do
31
38
  result result_type, :required
32
39
  message_to_user :string, :required, "Message to the user about what was done"
33
40
  end
34
41
 
42
+ klass.add_inputs klass.built_result_type
43
+
35
44
  klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
36
45
  "result formatted according to the " \
37
46
  "result schema and a message to the user. " \
38
47
  "The user might issue a new goal."
39
48
  else
40
- klass.add_inputs do
41
- result result_type, :required
49
+ if result_type.extends?(BuiltinTypes[:attributes])
50
+ klass.built_result_type = result_type
51
+ klass.result_is_attributes = true
52
+ klass.add_inputs result_type
53
+ else
54
+ klass.built_result_type = domain.foobara_type_from_declaration do
55
+ result result_type, :required
56
+ end
57
+ klass.add_inputs klass.built_result_type
42
58
  end
43
59
 
44
- klass.result result_type
45
-
46
60
  klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
47
61
  "result formatted according to the " \
48
62
  "result schema. " \
49
63
  "The user might issue a new goal."
50
64
  end
51
65
  elsif include_message_to_user_in_result
52
- klass.add_inputs do
53
- message_to_user :string, :required, "Message to the user about what was done"
54
- end
66
+ klass.returns_message_to_user = true
55
67
 
56
- klass.result do
68
+ klass.add_inputs do
57
69
  message_to_user :string, :required, "Message to the user about what was done"
58
70
  end
59
71
 
@@ -68,9 +80,46 @@ module Foobara
68
80
  # :nocov:
69
81
  end
70
82
 
83
+ apply_result_data_transformer(klass, result_entity_depth)
84
+
71
85
  klass
72
86
  end
73
87
  end
88
+
89
+ def result_data_transformer(result_entity_depth)
90
+ if result_entity_depth
91
+ case result_entity_depth
92
+ when AssociationDepth::AGGREGATE
93
+ CommandConnectors::Transformers::LoadAggregatesTransformer
94
+ when AssociationDepth::ATOM
95
+ CommandConnectors::Transformers::LoadAtomsTransformer
96
+ else
97
+ # :nocov:
98
+ raise "Unsupported result entity depth: #{result_entity_depth}"
99
+ # :nocov:
100
+ end&.instance
101
+ end
102
+ end
103
+
104
+ def apply_result_data_transformer(command_klass, result_entity_depth)
105
+ transformer = result_data_transformer(result_entity_depth)
106
+ return unless transformer
107
+
108
+ inputs_type = command_klass.inputs_type
109
+ return unless inputs_type
110
+
111
+ if inputs_type.extends?(BuiltinTypes[:detached_entity]) ||
112
+ DetachedEntity.contains_associations?(inputs_type)
113
+ command_klass.before_commit_transaction do |command:, **|
114
+ # TODO: why can't we just pass in the command??
115
+ built_result = command.built_result
116
+
117
+ if built_result
118
+ transformer.process_value!(built_result)
119
+ end
120
+ end
121
+ end
122
+ end
74
123
  end
75
124
 
76
125
  description "Ends the session giving a final result formatted according to the " \
@@ -81,24 +130,46 @@ module Foobara
81
130
  end
82
131
 
83
132
  def execute
133
+ build_result
84
134
  mark_mission_accomplished
85
135
 
86
- parsed_result
136
+ nil
87
137
  end
88
138
 
139
+ attr_accessor :built_result
140
+
89
141
  def mark_mission_accomplished
90
- command_connector.mark_mission_accomplished(inputs[:result], inputs[:message_to_user])
142
+ result, message_to_user = if returns_message_to_user?
143
+ [built_result[:result], built_result[:message_to_user]]
144
+ elsif returns_result_data?
145
+ [built_result, nil]
146
+ end
147
+
148
+ command_connector.mark_mission_accomplished(result, message_to_user)
91
149
  end
92
150
 
93
- def parsed_result
94
- inputs_type = self.class.inputs_type
95
- element_types = inputs_type.element_types
151
+ def build_result
152
+ self.built_result = if returns_message_to_user?
153
+ inputs.slice(:result, :message_to_user)
154
+ elsif returns_result_data?
155
+ if result_is_attributes?
156
+ inputs.slice(*self.class.built_result_type.element_types.keys)
157
+ else
158
+ inputs[:result]
159
+ end
160
+ end
161
+ end
96
162
 
97
- if element_types.key?(:message_to_user)
98
- inputs.slice(:result, :message_to_user)
99
- elsif element_types.key?(:result)
100
- inputs[:result]
101
- end
163
+ def returns_message_to_user?
164
+ self.class.returns_message_to_user
165
+ end
166
+
167
+ def returns_result_data?
168
+ self.class.returns_result_data
169
+ end
170
+
171
+ def result_is_attributes?
172
+ self.class.result_is_attributes
102
173
  end
103
174
  end
104
175
  end
@@ -11,6 +11,17 @@ module Foobara
11
11
  errors_hash :duck, "Errors that occurred during the command"
12
12
  end
13
13
  end
14
+
15
+ def success?
16
+ outcome[:success]
17
+ end
18
+
19
+ def errors_hash
20
+ # TODO: test this path
21
+ # :nocov:
22
+ outcome[:errors_hash]
23
+ # :nocov:
24
+ end
14
25
  end
15
26
  end
16
27
  end
data/src/foobara/agent.rb CHANGED
@@ -23,7 +23,8 @@ module Foobara
23
23
  :verbose,
24
24
  :io_out,
25
25
  :io_err,
26
- :max_llm_calls_per_minute
26
+ :max_llm_calls_per_minute,
27
+ :result_entity_depth
27
28
 
28
29
  def initialize(
29
30
  context: nil,
@@ -36,18 +37,20 @@ module Foobara
36
37
  io_out: nil,
37
38
  io_err: nil,
38
39
  max_llm_calls_per_minute: nil,
40
+ result_entity_depth: AssociationDepth::AGGREGATE,
39
41
  **opts
40
42
  )
41
43
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
42
44
  self.context = context
43
45
  self.agent_name = agent_name || "Anon#{SecureRandom.hex(2)}"
44
- self.llm_model = llm_model
46
+ self.llm_model = llm_model || "claude-opus-4-20250514"
45
47
  self.result_type = result_type
46
48
  self.include_message_to_user_in_result = include_message_to_user_in_result
47
49
  self.verbose = verbose
48
50
  self.io_out = io_out
49
51
  self.io_err = io_err
50
52
  self.max_llm_calls_per_minute = max_llm_calls_per_minute
53
+ self.result_entity_depth = result_entity_depth
51
54
 
52
55
  # unless opts.key?(:default_serializers)
53
56
  # opts = opts.merge(default_serializers: [
@@ -140,42 +143,7 @@ module Foobara
140
143
  state_machine.perform_transition!(:accomplish_goal)
141
144
 
142
145
  begin
143
- inputs = {
144
- goal:,
145
- final_result_type: self.result_type,
146
- context:,
147
- agent: self
148
- }
149
-
150
- llm_model ||= self.llm_model
151
-
152
- if llm_model
153
- inputs[:llm_model] = llm_model
154
- end
155
-
156
- unless maximum_call_count.nil?
157
- inputs[:maximum_command_calls] = maximum_call_count
158
- end
159
-
160
- if verbose
161
- inputs[:verbose] = verbose
162
- end
163
-
164
- if io_out
165
- inputs[:io_out] = io_out
166
- end
167
-
168
- if io_err
169
- inputs[:io_err] = io_err
170
- end
171
-
172
- if include_message_to_user_in_result || include_message_to_user_in_result == false
173
- inputs[:include_message_to_user_in_result] = include_message_to_user_in_result
174
- end
175
-
176
- if max_llm_calls_per_minute && max_llm_calls_per_minute > 0
177
- inputs[:max_llm_calls_per_minute] = max_llm_calls_per_minute
178
- end
146
+ inputs = accomplish_goal_inputs(goal, result_type:, maximum_call_count:, llm_model:)
179
147
 
180
148
  self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
181
149
 
@@ -196,6 +164,54 @@ module Foobara
196
164
  end
197
165
  end
198
166
 
167
+ def accomplish_goal_inputs(goal,
168
+ result_type: nil,
169
+ maximum_call_count: nil,
170
+ llm_model: nil)
171
+ inputs = {
172
+ goal:,
173
+ final_result_type: self.result_type,
174
+ context:,
175
+ agent: self
176
+ }
177
+
178
+ llm_model ||= self.llm_model
179
+
180
+ if llm_model
181
+ inputs[:llm_model] = llm_model
182
+ end
183
+
184
+ unless maximum_call_count.nil?
185
+ inputs[:maximum_command_calls] = maximum_call_count
186
+ end
187
+
188
+ if verbose
189
+ inputs[:verbose] = verbose
190
+ end
191
+
192
+ if io_out
193
+ inputs[:io_out] = io_out
194
+ end
195
+
196
+ if io_err
197
+ inputs[:io_err] = io_err
198
+ end
199
+
200
+ if include_message_to_user_in_result || include_message_to_user_in_result == false
201
+ inputs[:include_message_to_user_in_result] = include_message_to_user_in_result
202
+ end
203
+
204
+ if max_llm_calls_per_minute && max_llm_calls_per_minute > 0
205
+ inputs[:max_llm_calls_per_minute] = max_llm_calls_per_minute
206
+ end
207
+
208
+ if result_entity_depth
209
+ inputs[:result_entity_depth] = result_entity_depth
210
+ end
211
+
212
+ inputs
213
+ end
214
+
199
215
  def set_context_goal(goal)
200
216
  if context
201
217
  context.set_new_goal(goal)
@@ -232,7 +248,8 @@ module Foobara
232
248
  result_type:,
233
249
  agent_id: agent_name,
234
250
  # TODO: Support changing this flag when the goal changes
235
- include_message_to_user_in_result:
251
+ include_message_to_user_in_result:,
252
+ result_entity_depth:
236
253
  )
237
254
  else
238
255
  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.10
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi