foobara-agent 0.0.11 → 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: 0e0f6302247bbcc56ed93f61ec3a5d39e5041029e03b20cadea2d1dd0ebd04b6
4
- data.tar.gz: baca677e1ea555bcdc908fd2b37dc2d8b016df9888f8f7a69c8cff73774756b2
3
+ metadata.gz: dc4b3804b5930b121fdc679ecf203b9e8b4d11122d24da967cfbfd10a1e6562d
4
+ data.tar.gz: '0739391b9b59d453783756a8efed38ec3d559ce7636873776ee71a14027059b5'
5
5
  SHA512:
6
- metadata.gz: 350feb6d9058009889a342a5dcc4f44befed8a25be71ebc7673bf75b7ceb1f95adcec3a52d2eb53ba9301d0ef2312919a4496a53f2146423dd36c17b122856af
7
- data.tar.gz: a91ec6608bc3001f06d3c17562321d2c1a52a537372347593293d57d9d6312126f5116853c8b32b649d7c65a050bbb67bd03b2dbc50247cdc7260013b6d8f3ec
6
+ metadata.gz: 0ff04dba39a3edae5a80a98234cd7708c2f90d30bded9f12a0e2b665ea640c302e1ed14c607d61533daa5a0b3d7712684d92de740a3f3e085a1704286f95b837
7
+ data.tar.gz: 252b0f1a1c298bd984a856f4e63e57808b63cace046d58cf82f8c55f391c6d21fb3d98b8dda869582aa995eb6438a2fe998bae3952c5f6791eca00d25c5d181c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
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
+
1
12
  ## [0.0.11] - 2025-07-01
2
13
 
3
14
  - Fix some bugs with result type/value mismatches
@@ -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, :returns_message_to_user, :returns_result_data, :result_is_attributes
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
 
@@ -20,39 +23,40 @@ module Foobara
20
23
 
21
24
  if result_type
22
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
23
31
 
24
32
  # TODO: fix this... agent backed command sets these via its own result type.
25
33
  # check if message_to_user is already here and also search/fix result_data to be result for consistency.
26
34
  if include_message_to_user_in_result
27
35
  klass.returns_message_to_user = true
28
36
 
29
- klass.result do
37
+ klass.built_result_type = domain.foobara_type_from_declaration do
30
38
  result result_type, :required
31
39
  message_to_user :string, :required, "Message to the user about what was done"
32
40
  end
33
41
 
34
- klass.add_inputs klass.result_type
42
+ klass.add_inputs klass.built_result_type
35
43
 
36
44
  klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
37
45
  "result formatted according to the " \
38
46
  "result schema and a message to the user. " \
39
47
  "The user might issue a new goal."
40
48
  else
41
- unless result_type.is_a?(Types::Type)
42
- result_type = Domain.current.foobara_type_from_declaration(result_type)
43
- end
44
-
45
49
  if result_type.extends?(BuiltinTypes[:attributes])
50
+ klass.built_result_type = result_type
46
51
  klass.result_is_attributes = true
47
52
  klass.add_inputs result_type
48
53
  else
49
- klass.add_inputs do
54
+ klass.built_result_type = domain.foobara_type_from_declaration do
50
55
  result result_type, :required
51
56
  end
57
+ klass.add_inputs klass.built_result_type
52
58
  end
53
59
 
54
- klass.result result_type
55
-
56
60
  klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
57
61
  "result formatted according to the " \
58
62
  "result schema. " \
@@ -65,10 +69,6 @@ module Foobara
65
69
  message_to_user :string, :required, "Message to the user about what was done"
66
70
  end
67
71
 
68
- klass.result do
69
- message_to_user :string, :required, "Message to the user about what was done"
70
- end
71
-
72
72
  klass.description "Notifies the user that the current goal has been accomplished and results in a " \
73
73
  "message to the user. " \
74
74
  "The user might issue a new goal."
@@ -80,9 +80,46 @@ module Foobara
80
80
  # :nocov:
81
81
  end
82
82
 
83
+ apply_result_data_transformer(klass, result_entity_depth)
84
+
83
85
  klass
84
86
  end
85
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
86
123
  end
87
124
 
88
125
  description "Ends the session giving a final result formatted according to the " \
@@ -96,7 +133,7 @@ module Foobara
96
133
  build_result
97
134
  mark_mission_accomplished
98
135
 
99
- built_result
136
+ nil
100
137
  end
101
138
 
102
139
  attr_accessor :built_result
@@ -116,7 +153,7 @@ module Foobara
116
153
  inputs.slice(:result, :message_to_user)
117
154
  elsif returns_result_data?
118
155
  if result_is_attributes?
119
- inputs.slice(*self.class.result_type.element_types.keys)
156
+ inputs.slice(*self.class.built_result_type.element_types.keys)
120
157
  else
121
158
  inputs[:result]
122
159
  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,6 +37,7 @@ 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
@@ -48,6 +50,7 @@ module Foobara
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.11
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi