foobara-agent 0.0.18 → 0.0.20

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: ff591852377b5821eabf7ff4b49c4cc4bcacbebfdceaed539af14b831aa9cea3
4
- data.tar.gz: 35de585c33d57e7f52f91baf95efaccd685e42fb342a0ac407875c8eb1790dad
3
+ metadata.gz: 17773b8b47ace046d920b37565e0b5327fad2bba050f867c9a951ee89b780a93
4
+ data.tar.gz: 861120d98c2c9cce6f5194403cc950d7ff6c096061be4ebd8cd7a3f93f43c78f
5
5
  SHA512:
6
- metadata.gz: 3ba055dccfe9e32a9ec6a9397a47707de6804d43c1e0e99a23f798951f246ea158b08048b939b51278cf0f552d16268cd83fe22a46bc2915f3434de693a1040e
7
- data.tar.gz: '091f144c40b376ee7431c9ba4e906258f6d68704ebee1b568383bb66f7708cb40abeb4fab0650aeafd9bc2a9bb2b69f0428014566783f966d2545e07cfd0eb6e'
6
+ metadata.gz: be3168705a2ef7bfcc7a9ab2cde4713c91451857fbb762f121ada60d48cf590d5b0c184ca514221c58fb2b2bdbef08e20f95b7783ba54e83cb9f55fa37546350
7
+ data.tar.gz: c02c9a0b4dd95b228a8d723ed84af909040f50c8bb2694aea0b30e30a1ab7356f5f074d6a8e64a1ba47b5a113b69e013ad1db0f5fb98ded799ffcc8d5e89760d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.0.20] - 2025-07-23
2
+
3
+ - Fix bug with result type being an array without any message to the user
4
+
5
+ ## [0.0.19] - 2025-07-23
6
+
7
+ - Skip result: input if result is a Model and properly cast result
8
+ - Add include_message_to_user_in_result option to #accomplish_goal
9
+ - Include the llm_model in the verbose output
10
+ - Print out info about retries if verbose
11
+ - Change the language of the instructions to see what happens
12
+
1
13
  ## [0.0.18] - 2025-07-21
2
14
 
3
15
  - Add maximum_command_calls option to Agent#initialize
@@ -42,9 +42,8 @@ module Foobara
42
42
  simulate_describe_list_commands_command
43
43
  simulate_list_commands_run
44
44
  end
45
- # simulate_describe_command_run_for_all_commands
46
45
 
47
- until mission_accomplished or given_up or killed
46
+ until mission_accomplished? or given_up? or killed?
48
47
  increment_command_calls
49
48
  check_if_too_many_calls
50
49
 
@@ -53,7 +52,7 @@ module Foobara
53
52
  run_next_command
54
53
  end
55
54
 
56
- if given_up
55
+ if given_up?
57
56
  add_given_up_error
58
57
  end
59
58
 
@@ -124,9 +123,17 @@ module Foobara
124
123
  # :nocov:
125
124
  end
126
125
 
127
- def determine_next_command_and_inputs(retries = 3, error_outcome = nil)
126
+ RETRY_COUNT = 3
127
+
128
+ def determine_next_command_and_inputs(retries = RETRY_COUNT, error_outcome = nil)
128
129
  return if killed
129
130
 
131
+ if verbose? && retries != RETRY_COUNT
132
+ # :nocov:
133
+ (io_err || $stderr).puts " !!! Retrying to determine next command and inputs. Retries left: #{retries}"
134
+ # :nocov:
135
+ end
136
+
130
137
  if retries == 0
131
138
  # TODO: test this path by irreparably breaking the needed commands
132
139
  # :nocov:
@@ -244,7 +251,8 @@ module Foobara
244
251
  inputs_type = next_command_class.inputs_type
245
252
 
246
253
  NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
247
- inputs_type.process_value(next_command_inputs)
254
+ inputs = next_command_inputs.nil? ? {} : next_command_inputs
255
+ inputs_type.process_value(inputs)
248
256
  end
249
257
  end
250
258
 
@@ -425,7 +433,16 @@ module Foobara
425
433
  end
426
434
 
427
435
  agent_name = agent.agent_name
428
- prefix = agent_name && !agent_name.empty? ? "#{agent_name}: " : ""
436
+ prefix = if agent_name && !agent_name.empty?
437
+ "#{agent_name}<#{llm_model}>: "
438
+ else
439
+ # Not setting an agent_name results in non-determinism in NotifyUser... command
440
+ # namespace. This results in problems in the test suite making this code path a little
441
+ # harder to test indirectly.
442
+ # :nocov:
443
+ ""
444
+ # :nocov:
445
+ end
429
446
 
430
447
  (io_out || $stdout).puts "#{prefix}#{next_command_name}.run#{args}"
431
448
  end
@@ -451,10 +468,40 @@ module Foobara
451
468
  add_runtime_error(:gave_up, reason: final_message)
452
469
  end
453
470
 
471
+ def final_result_type_from_declaration
472
+ return @final_result_type_from_declaration if defined?(@final_result_type_from_declaration)
473
+
474
+ @final_result_type_from_declaration = if final_result_type
475
+ if final_result_type.is_a?(Types::Type)
476
+ final_result_type
477
+ else
478
+ domain = result_type.created_in_namespace.foobara_domain
479
+ domain.foobara_type_from_declaration(final_result_type)
480
+ end
481
+ end
482
+ end
483
+
454
484
  def build_result
485
+ result_data = if final_result_type_from_declaration
486
+ outcome = final_result_type_from_declaration.process_value(final_result)
487
+
488
+ if outcome.success?
489
+ outcome.result
490
+ elsif given_up? || killed?
491
+ final_result
492
+ else
493
+ # :nocov:
494
+ raise CommandPatternImplementation::Concerns::Result::CouldNotProcessResult,
495
+ outcome.errors
496
+ # :nocov:
497
+ end
498
+ else
499
+ final_result
500
+ end
501
+
455
502
  {
456
503
  message_to_user: final_message,
457
- result_data: final_result
504
+ result_data:
458
505
  }
459
506
  end
460
507
 
@@ -497,6 +544,18 @@ module Foobara
497
544
  def context
498
545
  agent.context
499
546
  end
547
+
548
+ def mission_accomplished?
549
+ mission_accomplished
550
+ end
551
+
552
+ def given_up?
553
+ given_up
554
+ end
555
+
556
+ def killed?
557
+ killed
558
+ end
500
559
  end
501
560
  end
502
561
  end
@@ -17,15 +17,8 @@ module Foobara
17
17
  end
18
18
 
19
19
  def build_llm_instructions(assistant_association_depth, goal, previous_goals)
20
- instructions = "You are the implementation of a command called #{scoped_full_name}"
21
-
22
- instructions += if description && !description.empty?
23
- " which has the following description:\n\n#{description}\n\n"
24
- else
25
- # :nocov:
26
- ". "
27
- # :nocov:
28
- end
20
+ instructions = "You are part of an agent implementation. " \
21
+ "Your task is to decide which command to run next.\n\n"
29
22
 
30
23
  result_schema = result_json_schema(assistant_association_depth)
31
24
 
@@ -38,13 +31,25 @@ module Foobara
38
31
  end
39
32
  end
40
33
 
41
- instructions += "You are working towards accomplishing the following goal:\n\n#{goal}\n\n"
34
+ instructions += "You are working towards accomplishing the following goal: #{goal}\n\n"
35
+
36
+ instructions += "You will answer with the name of the next command to run and, if needed, its inputs. " \
37
+ "Choose whichever command is best to make progress towards accomplishing the current goal " \
38
+ "based on the progress made so far. " \
39
+ "If the goal has been accomplished then choose the " \
40
+ "NotifyUserThatCurrentGoalHasBeenAccomplished command. " \
41
+ "If you are stuck either due to errors " \
42
+ "or because you do not have the command you need to accomplish the " \
43
+ "goal, then choose GiveUp.\n\n"
42
44
 
43
- instructions += "Your response of which command to run next should match the following JSON schema:"
44
- instructions += "\n\n#{result_schema}\n\n"
45
45
  instructions += "You can get more details about the inputs and result schemas for a specific command by " \
46
- "choosing the DescribeCommand command. " \
47
- "You will reply with nothing more than the JSON you've generated so that the calling code " \
46
+ "choosing the DescribeCommand command.\n\n"
47
+
48
+ instructions += "Your result type is described by the following JSON schema:" \
49
+ "\n\n#{result_schema}\n\n" \
50
+ "You will reply with nothing more than the JSON that you've " \
51
+ "generated that adheres to this result type schema " \
52
+ "so that the calling code " \
48
53
  "can successfully parse your answer."
49
54
 
50
55
  instructions
@@ -54,7 +59,8 @@ module Foobara
54
59
  description "Returns the name of the next command to run and its inputs given the progress " \
55
60
  "towards accomplishing the current goal. " \
56
61
  "If the goal has been accomplished it will choose the " \
57
- "NotifyUserThatCurrentGoalHasBeenAccomplished command."
62
+ "NotifyUserThatCurrentGoalHasBeenAccomplished command. It will choose GiveUp if it runs into an " \
63
+ "error it cannot get around or does not believe it has the proper commands to accomplish its goal."
58
64
 
59
65
  result do
60
66
  command :string, :required
@@ -7,7 +7,7 @@ module Foobara
7
7
 
8
8
  class << self
9
9
  attr_accessor :command_class, :returns_message_to_user, :returns_result_data, :result_is_attributes,
10
- :built_result_type
10
+ :built_result_type, :result_is_model
11
11
 
12
12
  def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true, result_entity_depth: nil)
13
13
  agent_id ||= "Anon#{SecureRandom.hex(2)}"
@@ -46,20 +46,38 @@ module Foobara
46
46
  "result schema and a message to the user. " \
47
47
  "The user might issue a new goal."
48
48
  else
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
49
+ if result_type.extends?(BuiltinTypes[:model]) &&
50
+ !result_type.extends?(BuiltinTypes[:detached_entity]) && result_type != BuiltinTypes[:model]
51
+ # TODO: Create a ModelToAttributesInputsTransformer in foobara gem and move this logic there
52
+ model_declaration = result_type.declaration_data
53
+
54
+ # Distinguishing between nil and no inputs might be tricky so if :allow_nil then
55
+ # we will prefer `result: nil` over `nil`
56
+ # if there's a description we'll also prefer result: so that the description will
57
+ # make it through to the LLM on that input.
58
+ # TODO: handle the model's description via the command's description instead.
59
+ if !model_declaration.key?(:allow_nil) && !model_declaration.key?(:description)
60
+ klass.built_result_type = result_type
61
+ klass.result_is_model = true
62
+ klass.add_inputs result_type.target_class.attributes_type
56
63
  end
57
- klass.add_inputs klass.built_result_type
58
64
  end
59
65
 
60
- klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
61
- "result formatted according to the " \
62
- "result schema. " \
66
+ unless klass.result_is_model
67
+ if result_type.extends?(BuiltinTypes[:attributes])
68
+ klass.built_result_type = result_type
69
+ klass.result_is_attributes = true
70
+ klass.add_inputs result_type
71
+ else
72
+ klass.built_result_type = domain.foobara_type_from_declaration do
73
+ result result_type, :required
74
+ end
75
+ klass.add_inputs klass.built_result_type
76
+ end
77
+ end
78
+
79
+ klass.description "Notifies the user that the current goal has been accomplished and accepts a final " \
80
+ "result value as inputs. " \
63
81
  "The user might issue a new goal."
64
82
  end
65
83
  elsif include_message_to_user_in_result
@@ -112,7 +130,7 @@ module Foobara
112
130
  DetachedEntity.contains_associations?(inputs_type)
113
131
  command_klass.before_commit_transaction do |command:, **|
114
132
  # TODO: why can't we just pass in the command??
115
- built_result = command.built_result
133
+ built_result = command.final_result_data
116
134
 
117
135
  if built_result
118
136
  transformer.process_value!(built_result)
@@ -130,34 +148,35 @@ module Foobara
130
148
  end
131
149
 
132
150
  def execute
133
- build_result
151
+ extract_result_data_and_message_to_user
134
152
  mark_mission_accomplished
135
153
 
136
154
  nil
137
155
  end
138
156
 
139
- attr_accessor :built_result
140
-
141
- def mark_mission_accomplished
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
157
+ attr_accessor :final_result_data, :final_message_to_user
158
+
159
+ def extract_result_data_and_message_to_user
160
+ result, message = if returns_message_to_user?
161
+ inputs.values_at(:result, :message_to_user)
162
+ elsif returns_result_data?
163
+ if result_is_attributes?
164
+ inputs.slice(*self.class.built_result_type.element_types.keys)
165
+ elsif result_is_model?
166
+ inputs.slice(
167
+ *self.class.built_result_type.target_class.attributes_type.element_types.keys
168
+ )
169
+ else
170
+ [inputs[:result], nil]
171
+ end
172
+ end
147
173
 
148
- command_connector.mark_mission_accomplished(result, message_to_user)
174
+ self.final_result_data = result
175
+ self.final_message_to_user = message
149
176
  end
150
177
 
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
178
+ def mark_mission_accomplished
179
+ command_connector.mark_mission_accomplished(final_result_data, final_message_to_user)
161
180
  end
162
181
 
163
182
  def returns_message_to_user?
@@ -171,6 +190,10 @@ module Foobara
171
190
  def result_is_attributes?
172
191
  self.class.result_is_attributes
173
192
  end
193
+
194
+ def result_is_model?
195
+ self.class.result_is_model
196
+ end
174
197
  end
175
198
  end
176
199
  end
@@ -3,12 +3,6 @@ require_relative "goal"
3
3
  module Foobara
4
4
  class Agent < CommandConnector
5
5
  class Context < Foobara::Model
6
- class << self
7
- def for(goal)
8
- new(command_log: [], current_goal: Goal.new(text: goal))
9
- end
10
- end
11
-
12
6
  attributes do
13
7
  current_goal Goal, :required, "The current goal the agent needs to accomplish"
14
8
  previous_goals [Goal]
@@ -19,7 +13,11 @@ module Foobara
19
13
 
20
14
  def set_new_goal(goal)
21
15
  self.previous_goals ||= []
22
- previous_goals << current_goal
16
+
17
+ if current_goal
18
+ previous_goals << current_goal
19
+ end
20
+
23
21
  self.current_goal = Goal.new(text: goal)
24
22
  end
25
23
  end
data/src/foobara/agent.rb CHANGED
@@ -44,7 +44,8 @@ module Foobara
44
44
  **opts
45
45
  )
46
46
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
47
- self.context = context
47
+ self.context = context || Context.new(command_log: [])
48
+
48
49
  if agent_name
49
50
  self.agent_name = agent_name
50
51
  end
@@ -125,8 +126,13 @@ module Foobara
125
126
  goal,
126
127
  result_type: nil,
127
128
  maximum_command_calls: nil,
129
+ include_message_to_user_in_result: nil,
128
130
  llm_model: nil
129
131
  )
132
+ unless include_message_to_user_in_result.nil?
133
+ self.include_message_to_user_in_result = include_message_to_user_in_result
134
+ end
135
+
130
136
  set_context_goal(goal)
131
137
 
132
138
  if result_type && self.result_type != result_type
@@ -150,7 +156,10 @@ module Foobara
150
156
  state_machine.perform_transition!(:accomplish_goal)
151
157
 
152
158
  begin
153
- inputs = accomplish_goal_inputs(goal, result_type:, maximum_command_calls:, llm_model:)
159
+ inputs = accomplish_goal_inputs(goal,
160
+ result_type:,
161
+ maximum_command_calls:,
162
+ llm_model:)
154
163
 
155
164
  self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
156
165
 
@@ -236,11 +245,7 @@ module Foobara
236
245
  end
237
246
 
238
247
  def set_context_goal(goal)
239
- if context
240
- context.set_new_goal(goal)
241
- else
242
- self.context = Context.for(goal)
243
- end
248
+ context.set_new_goal(goal)
244
249
  end
245
250
 
246
251
  def mark_mission_accomplished(final_result, message_to_user)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.18
4
+ version: 0.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi