foobara-agent 0.0.5 → 0.0.7

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: 88347fd73d897f4a3f05eb2dcec999c5afddcec625cecc79a44a1a803394b352
4
- data.tar.gz: '016936d6acc4e2497aa8e8853d058653d201d3d7d5c2c77090128b728f8833fe'
3
+ metadata.gz: 968399c2d338c8f069712d0ad9437b928fe48bc7356e88fd86b9ff6fb7ae235f
4
+ data.tar.gz: 8483b856faf2f559985550cc631764121797f88963c0baf428b7f16646bd8b59
5
5
  SHA512:
6
- metadata.gz: 5672d6bd7930969b566d6dce04314062116593cd2df5c06d718916d83938deb91af64765a85752f7b5c8119d0fce32a56099907b252da636f0408fb3109bf38c
7
- data.tar.gz: 71b2c350e024aca9d86c1f5f2bfd3d83726784ab028c6d7b6e0010a88512c1a06351cfbb298478fc1e5f6b5126367e9bca5d417040b9d493153fe7d142b20faa
6
+ metadata.gz: 79be2912847cb407d1ad65166313c8b799b981a2b0da6f8c8e3f8132049c1eb67ec56a9307fdf887f8e659299883f0da4517762abc885fdf8b69949f869f54de
7
+ data.tar.gz: 169c02a8070d5feb4d307b41ff757294ca5a2e3b810ac0766cece386a357db2cf85075ae9ecb1f5ee2e0b1ec92c855eb202ee4166102a88341b8e5b4268eeb92
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.0.7] - 2025-06-23
2
+
3
+ -
4
+
5
+ ## [0.0.6] - 2025-06-19
6
+
7
+ - Do not run next command with pre-cast inputs
8
+ - Make result_data and message_to_user required (probably unwise for message_to_user?)
9
+ - Simulate ListCommands and DescribeCommand being at start
10
+ - Add a verbose flag for debugging help
11
+ - Give each AccomplishGoal its own command call count
12
+ - Convert entities to their aggregates as input and primary keys as output
13
+
1
14
  ## [0.0.5] - 2025-05-30
2
15
 
3
16
  - Don't require agents to have a name
@@ -1,7 +1,6 @@
1
1
  require_relative "list_commands"
2
2
 
3
3
  module Foobara
4
- # TODO: should agent maybe be a command connector? It feels a bit more like a command connector.
5
4
  class Agent < CommandConnector
6
5
  class AccomplishGoal < Foobara::Command
7
6
  possible_error :gave_up, context: { reason: :string }, message: "Gave up."
@@ -9,13 +8,14 @@ module Foobara
9
8
  context: { maximum_command_calls: :integer }
10
9
 
11
10
  inputs do
12
- agent_name :string, "Name of the agent"
13
11
  goal :string, :required, "What do you want the agent to attempt to accomplish?"
14
12
  # TODO: we should be able to specify a subclass as a type
15
- command_classes [Class], "Commands that can be ran to accomplish the goal"
16
13
  final_result_type :duck, "Specifies how the result of the goal is to be structured"
17
- existing_command_connector CommandConnector, :allow_nil,
18
- "A connector containing already-connected commands for the agent to use"
14
+ include_message_to_user_in_result :boolean, default: true
15
+ verbose :boolean, default: false
16
+ io_out :duck
17
+ io_err :duck
18
+ agent Agent, :required
19
19
  current_context Context, :allow_nil, "The current context of the agent"
20
20
  maximum_command_calls :integer,
21
21
  :allow_nil,
@@ -42,7 +42,7 @@ module Foobara
42
42
  end
43
43
 
44
44
  result do
45
- message_to_user :string, :required, "Message to the user about successfully accomplishing the goal"
45
+ message_to_user :string, :allow_nil, "Message to the user about successfully accomplishing the goal"
46
46
  result_data :duck, "Optional result data to return to the user if final_result_type was given"
47
47
  end
48
48
 
@@ -51,19 +51,13 @@ module Foobara
51
51
  def execute
52
52
  build_initial_context_if_necessary
53
53
 
54
- if command_connector_passed_in?
55
- set_accomplished_goal_command
56
- else
57
- build_command_connector
58
- connect_user_provided_commands
59
- end
60
-
61
- unless agent_commands_connected?
62
- connect_agent_commands
63
- end
54
+ simulate_list_commands_run
55
+ simulate_describe_command_run_for_all_commands
64
56
 
65
57
  until mission_accomplished or given_up
58
+ increment_command_calls
66
59
  check_if_too_many_calls
60
+
67
61
  if choose_next_command_and_next_inputs_separately?
68
62
  determine_next_command_then_inputs_separately
69
63
  else
@@ -81,71 +75,43 @@ module Foobara
81
75
  build_result
82
76
  end
83
77
 
84
- def agent_commands_connected?
85
- command_connector.agent_commands_connected?
86
- end
87
-
88
- def validate
89
- validate_either_command_classes_or_connector_given
90
- end
91
-
92
- def validate_either_command_classes_or_connector_given
93
- # TODO: implement this!
94
- end
95
-
96
78
  attr_accessor :context, :next_command_name, :next_command_inputs, :mission_accomplished, :given_up,
97
79
  :next_command_class, :next_command, :command_outcome, :timed_out,
98
- :final_result, :final_message, :command_response, :delayed_command_name
99
- attr_writer :command_connector
100
-
101
- def agent_name
102
- @agent_name ||= inputs[:agent_name] || "Anon#{SecureRandom.hex(2)}"
103
- end
80
+ :final_result, :final_message, :command_response, :delayed_command_name,
81
+ :command_calls
104
82
 
105
83
  def build_initial_context_if_necessary
106
84
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
107
85
  self.context = current_context || Context.new(command_log: [])
108
86
  end
109
87
 
110
- def command_connector_passed_in?
111
- existing_command_connector
112
- end
88
+ def simulate_list_commands_run
89
+ return unless context.command_log.empty?
113
90
 
114
- def command_connector
115
- @command_connector ||= existing_command_connector
116
- end
91
+ self.next_command_name = ListCommands.full_command_name
92
+ self.next_command_inputs = nil
93
+ fetch_next_command_class
117
94
 
118
- # Do we really want to support calling AccomplishGoal outside the context of an Agent?
119
- # If not, just delete this awkward coupling
120
- def build_command_connector
121
- self.command_connector ||= Agent.new(
122
- current_accomplish_goal_command: self,
123
- llm_model:
124
- )
95
+ run_next_command
96
+ log_last_command_outcome
125
97
  end
126
98
 
127
- def set_accomplished_goal_command
128
- command_connector.current_accomplish_goal_command = self
129
- end
99
+ def simulate_describe_command_run_for_all_commands
100
+ return if context.command_log.size > 1
130
101
 
131
- def connect_agent_commands
132
- command_connector.connect_agent_commands(final_result_type:, agent_name:)
133
- end
102
+ ListCommands.run!(command_connector: agent)[:user_provided_commands].each do |full_command_name|
103
+ next if described_commands.include?(full_command_name)
104
+
105
+ self.next_command_name = DescribeCommand.full_command_name
106
+ self.next_command_inputs = { command_name: full_command_name }
107
+ fetch_next_command_class
134
108
 
135
- def connect_user_provided_commands
136
- command_classes.each do |command_class|
137
- command_connector.connect(command_class)
109
+ run_next_command
110
+ log_last_command_outcome
138
111
  end
139
112
  end
140
113
 
141
114
  def determine_next_command_and_inputs(retries = 2)
142
- if context.command_log.empty?
143
- self.next_command_name = ListCommands.full_command_name
144
- self.next_command_inputs = nil
145
- fetch_next_command_class
146
- return
147
- end
148
-
149
115
  inputs_for_determine = {
150
116
  goal:,
151
117
  context:,
@@ -250,15 +216,9 @@ module Foobara
250
216
  def validate_next_command_inputs
251
217
  inputs_type = next_command_class.inputs_type
252
218
 
253
- outcome = NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
219
+ NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
254
220
  inputs_type.process_value(next_command_inputs)
255
221
  end
256
-
257
- if outcome.success?
258
- self.next_command_inputs = outcome.result
259
- end
260
-
261
- outcome
262
222
  end
263
223
 
264
224
  def command_name_type
@@ -266,9 +226,7 @@ module Foobara
266
226
  end
267
227
 
268
228
  def determine_next_command_name(retries = 2)
269
- self.next_command_name = if context.command_log.empty?
270
- ListCommands.full_command_name
271
- elsif delayed_command_name
229
+ self.next_command_name = if delayed_command_name
272
230
  name = delayed_command_name
273
231
  self.delayed_command_name = nil
274
232
  name
@@ -327,11 +285,11 @@ module Foobara
327
285
  end
328
286
 
329
287
  def all_command_classes
330
- @all_command_classes ||= run_subcommand!(ListCommands, command_connector:).values.flatten
288
+ @all_command_classes ||= run_subcommand!(ListCommands, command_connector: agent).values.flatten
331
289
  end
332
290
 
333
291
  def fetch_next_command_class
334
- self.next_command_class = command_connector.transformed_command_from_name(next_command_name)
292
+ self.next_command_class = agent.transformed_command_from_name(next_command_name)
335
293
  end
336
294
 
337
295
  def determine_next_command_inputs(retries = 2)
@@ -392,21 +350,42 @@ module Foobara
392
350
  end
393
351
 
394
352
  def run_next_command
395
- self.command_response = command_connector.run(
353
+ if verbose?
354
+ (io_out || $stdout).puts "Running #{next_command_name} with #{next_command_inputs}"
355
+ end
356
+
357
+ self.command_response = agent.run(
396
358
  full_command_name: next_command_name,
397
359
  inputs: next_command_inputs,
398
360
  action: "run"
399
361
  )
400
362
 
401
363
  self.command_outcome = command_response.outcome
364
+
365
+ if verbose?
366
+ if command_outcome.success?
367
+ (io_out || $stdout).puts "Command #{command_response.command.class.full_command_name} succeeded"
368
+ else
369
+ # :nocov:
370
+ (io_err || $stderr).puts(
371
+ "Command #{command_response.command.class.full_command_name} failed #{command_outcome.errors_hash}"
372
+ )
373
+ # :nocov:
374
+ end
375
+ end
402
376
  end
403
377
 
404
378
  def log_last_command_outcome
405
379
  log_command_outcome(command: command_response.command)
406
380
  end
407
381
 
382
+ def increment_command_calls
383
+ self.command_calls ||= -1
384
+ self.command_calls += 1
385
+ end
386
+
408
387
  def check_if_too_many_calls
409
- if context.command_log.size > maximum_command_calls
388
+ if command_calls > maximum_command_calls
410
389
  add_runtime_error(
411
390
  :too_many_command_calls,
412
391
  "Too many command calls. " \
@@ -485,6 +464,14 @@ module Foobara
485
464
  def choose_next_command_and_next_inputs_separately?
486
465
  choose_next_command_and_next_inputs_separately
487
466
  end
467
+
468
+ def agent_name
469
+ agent.agent_name
470
+ end
471
+
472
+ def verbose?
473
+ verbose
474
+ end
488
475
  end
489
476
  end
490
477
  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, "Optional message to the user"
22
- end
23
-
24
21
  if result_type
25
- add_inputs do
26
- result_data result_type
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
31
- result_data result_type
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, "Optional message to the user"
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,7 +18,11 @@ module Foobara
18
18
  :llm_model,
19
19
  :current_accomplish_goal_command,
20
20
  :result_type,
21
- :agent_commands_connected
21
+ :include_message_to_user_in_result,
22
+ :agent_commands_connected,
23
+ :verbose,
24
+ :io_out,
25
+ :io_err
22
26
 
23
27
  def initialize(
24
28
  context: nil,
@@ -26,25 +30,34 @@ module Foobara
26
30
  command_classes: nil,
27
31
  llm_model: nil,
28
32
  result_type: nil,
29
- current_accomplish_goal_command: nil,
33
+ include_message_to_user_in_result: true,
34
+ verbose: false,
35
+ io_out: nil,
36
+ io_err: nil,
30
37
  **opts
31
38
  )
32
39
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
33
40
  self.context = context
34
- self.agent_name = agent_name if agent_name
41
+ self.agent_name = agent_name || "Anon#{SecureRandom.hex(2)}"
35
42
  self.llm_model = llm_model
36
43
  self.result_type = result_type
37
- self.current_accomplish_goal_command = current_accomplish_goal_command
44
+ self.include_message_to_user_in_result = include_message_to_user_in_result
45
+ self.verbose = verbose
46
+ self.io_out = io_out
47
+ self.io_err = io_err
38
48
 
39
49
  unless opts.key?(:default_serializers)
40
50
  opts = opts.merge(default_serializers: [
41
51
  Foobara::CommandConnectors::Serializers::ErrorsSerializer,
42
- Foobara::CommandConnectors::Serializers::AtomicSerializer
52
+ Foobara::CommandConnectors::Serializers::AggregateSerializer
43
53
  ])
44
54
  end
45
55
 
46
56
  super(**opts)
47
57
 
58
+ # TODO: this should work now, switch to this approach
59
+ # add_default_inputs_transformer EntityToPrimaryKeyInputsTransformer
60
+
48
61
  build_initial_context
49
62
 
50
63
  # TODO: push this convenience method up into base class?
@@ -53,6 +66,20 @@ module Foobara
53
66
  end
54
67
  end
55
68
 
69
+ def connect(*args, **opts)
70
+ args, opts = desugarize_connect_args(args, opts)
71
+
72
+ inputs_transformers = opts[:inputs_transformers] || []
73
+ inputs_transformers = Util.array(inputs_transformers)
74
+ inputs_transformers << CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer
75
+
76
+ unless opts.key?(:aggregate_entities)
77
+ opts = opts.merge(aggregate_entities: true)
78
+ end
79
+
80
+ super(*args, **opts.merge(inputs_transformers:))
81
+ end
82
+
56
83
  def run(*args, **)
57
84
  if args.first.is_a?(::String)
58
85
  accomplish_goal(*args, **)
@@ -81,7 +108,8 @@ module Foobara
81
108
  goal,
82
109
  result_type: nil,
83
110
  choose_next_command_and_next_inputs_separately: nil,
84
- maximum_call_count: nil
111
+ maximum_call_count: nil,
112
+ llm_model: nil
85
113
  )
86
114
  if result_type && self.result_type != result_type
87
115
  if self.result_type
@@ -97,6 +125,10 @@ module Foobara
97
125
  end
98
126
  end
99
127
 
128
+ unless agent_commands_connected?
129
+ connect_agent_commands
130
+ end
131
+
100
132
  state_machine.perform_transition!(:accomplish_goal)
101
133
 
102
134
  begin
@@ -104,12 +136,10 @@ module Foobara
104
136
  goal:,
105
137
  final_result_type: self.result_type,
106
138
  current_context: context,
107
- existing_command_connector: self
139
+ agent: self
108
140
  }
109
141
 
110
- if agent_name
111
- inputs[:agent_name] = agent_name
112
- end
142
+ llm_model ||= self.llm_model
113
143
 
114
144
  if llm_model
115
145
  inputs[:llm_model] = llm_model
@@ -123,14 +153,32 @@ module Foobara
123
153
  inputs[:maximum_command_calls] = maximum_call_count
124
154
  end
125
155
 
156
+ if verbose
157
+ inputs[:verbose] = verbose
158
+ end
159
+
160
+ if io_out
161
+ inputs[:io_out] = io_out
162
+ end
163
+
164
+ if io_err
165
+ inputs[:io_err] = io_err
166
+ end
167
+
168
+ if include_message_to_user_in_result || include_message_to_user_in_result == false
169
+ inputs[:include_message_to_user_in_result] = include_message_to_user_in_result
170
+ end
171
+
126
172
  self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
127
173
 
128
174
  current_accomplish_goal_command.run.tap do |outcome|
129
- if outcome.success?
130
- state_machine.perform_transition!(:goal_accomplished)
131
- else
132
- state_machine.perform_transition!(:goal_errored)
133
- end
175
+ transition = if outcome.success?
176
+ :goal_accomplished
177
+ else
178
+ :goal_errored
179
+ end
180
+
181
+ state_machine.perform_transition!(transition)
134
182
  end
135
183
  rescue
136
184
  # :nocov:
@@ -159,19 +207,21 @@ module Foobara
159
207
  agent_commands_connected
160
208
  end
161
209
 
162
- def connect_agent_commands(final_result_type: nil, agent_name: nil)
210
+ def connect_agent_commands
163
211
  command_classes = [
164
212
  DescribeCommand,
165
213
  DescribeType,
166
214
  GiveUp,
167
- ListCommands,
168
- ListTypes
215
+ ListCommands
169
216
  ]
170
217
 
171
- command_classes << if final_result_type
218
+ command_classes << if result_type || include_message_to_user_in_result
219
+ # TODO: Support changing the final result type when the goal changes
172
220
  NotifyUserThatCurrentGoalHasBeenAccomplished.for(
173
- result_type: final_result_type,
174
- agent_id: agent_name
221
+ result_type:,
222
+ agent_id: agent_name,
223
+ # TODO: Support changing this flag when the goal changes
224
+ include_message_to_user_in_result:
175
225
  )
176
226
  else
177
227
  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.5
4
+ version: 0.0.7
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
@@ -85,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
84
  - !ruby/object:Gem::Version
86
85
  version: '0'
87
86
  requirements: []
88
- rubygems_version: 3.6.7
87
+ rubygems_version: 3.6.9
89
88
  specification_version: 4
90
89
  summary: An agent that uses whatever Foobara commands you wish to accomplish goals
91
90
  of your choosing!
@@ -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