foobara-agent 0.0.1 → 0.0.3

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: db38ddbbaba0842ca16f3155372979cae6ed6d2807c5dd2a62dcaf7f9b93941b
4
- data.tar.gz: 0415660f250abc75647efc61ed27635fd82c7cd2ceadb4d9ac93e1cf5bb4c3bf
3
+ metadata.gz: 88666f0c9353fd88d386752d774b3a5abac7bf035b9349769049a1e633fb6a4e
4
+ data.tar.gz: 60a09c5cd82d3c4b580ab3224875bc78b3086f04d91e3c28da6708483d4b3806
5
5
  SHA512:
6
- metadata.gz: a9c2401005ddc1f5cb03ed562a5a811b01d575234713d9d6c0cd4b720fc6a776637bb3195b0a96b38945a4b1c4bdeb862a2de12bb83d2fb7267e8fbd6f30903c
7
- data.tar.gz: abf74b0929795f22d434a813794b0f99516d1e817b57dfd9de31cd50833096c384c423deae386c68d91d9692e7b5eebad0739cd38d2e122dc8990c6c6fdb3822
6
+ metadata.gz: 5590b86e0dbb6baf8feac06ed1dce7a95a066acf3d684203dff92b86adb1fc329a217ec702affa332d9b6b6c2d6d187b5e3c960ee4b22a7263c0412760fd30c2
7
+ data.tar.gz: 13e01dd7118c05188ffd6a58ba51923eec81b14b227506a6fdeaf0c731dfef4bc41a14a052f2c17c2e46e3d32438a181ff906a082bf7f123ee7210fca1efc959
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.0.3] - 2025-05-30
2
+
3
+ - Increase the retries, attempt to improve command descriptions,
4
+ and attempt to improve the information stored in the context/command log
5
+ - Add some safeguards around changing the result type too late in the process
6
+ - Add an agent state machine and a kill! method
7
+
8
+ ## [0.0.2] - 2025-05-27
9
+
10
+ - Add maximum_call_count option
11
+ - Try getting the command and inputs together to reduce calls
12
+ - Tweaks to algorithms and improvements to what is stored in context/command_log
13
+
1
14
  ## [0.0.1] - 2025-05-21
2
15
 
3
16
  - Initial release
data/lib/foobara/agent.rb CHANGED
@@ -13,10 +13,10 @@ module Foobara
13
13
  [
14
14
  DetermineInputsForNextCommand,
15
15
  DetermineNextCommand,
16
- EndSessionBecauseGoalHasBeenAccomplished
16
+ NotifyUserThatCurrentGoalHasBeenAccomplished
17
17
  ].each do |command_class|
18
- command_class.clear_cache
19
- Util.descendants(command_class).each(&:clear_cache)
18
+ command_class.clear_subclass_cache
19
+ Util.descendants(command_class).each(&:clear_subclass_cache)
20
20
  end
21
21
  end
22
22
  end
@@ -5,20 +5,40 @@ module Foobara
5
5
  class Agent
6
6
  class AccomplishGoal < Foobara::Command
7
7
  possible_error :gave_up, context: { reason: :string }, message: "Gave up."
8
+ possible_error :too_many_command_calls,
9
+ context: { maximum_command_calls: :integer }
8
10
 
9
11
  inputs do
10
12
  agent_name :string, "Name of the agent"
11
13
  goal :string, :required, "What do you want the agent to attempt to accomplish?"
12
14
  # TODO: we should be able to specify a subclass as a type
13
- command_classes [:duck], "Commands that can be ran to accomplish the goal"
15
+ command_classes [Class], "Commands that can be ran to accomplish the goal"
14
16
  final_result_type :duck, "Specifies how the result of the goal is to be structured"
15
- existing_command_connector :duck, "A connector containing already-connected commands for the agent to use"
16
- current_context :duck, "The current context of the agent"
17
+ existing_command_connector CommandConnector, :allow_nil,
18
+ "A connector containing already-connected commands for the agent to use"
19
+ current_context Context, :allow_nil, "The current context of the agent"
20
+ maximum_command_calls :integer,
21
+ :allow_nil,
22
+ default: 25,
23
+ description: "Maximum number of commands to run before giving up"
17
24
  llm_model :string,
18
25
  :allow_nil,
19
26
  one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
20
27
  default: "claude-3-7-sonnet-20250219",
21
28
  description: "The model to use for the LLM"
29
+ log_successful_determine_command_and_inputs_outcomes(
30
+ :boolean,
31
+ default: true,
32
+ description: "You can experiment with turning this off " \
33
+ "if you want to see what happens if we don't log " \
34
+ "successful command/input selection outcomes"
35
+ )
36
+ choose_next_command_and_next_inputs_separately :boolean,
37
+ default: false,
38
+ description:
39
+ "By default, asks for next command and inputs together. " \
40
+ "You can experiment with getting the separately " \
41
+ "with this flag if you wish."
22
42
  end
23
43
 
24
44
  result do
@@ -36,26 +56,22 @@ module Foobara
36
56
  else
37
57
  build_command_connector
38
58
  connect_user_provided_commands
39
- connect_agent_commands
40
59
  end
41
60
 
42
- unless command_connector.agent_commands_connected?
61
+ unless agent_commands_connected?
43
62
  connect_agent_commands
44
63
  end
45
64
 
46
- until mission_accomplished or given_up or timed_out
47
- determine_next_command_name
48
-
49
- if command_described?
50
- fetch_next_command_class
51
- determine_next_command_inputs
65
+ until mission_accomplished or given_up
66
+ check_if_too_many_calls
67
+ if choose_next_command_and_next_inputs_separately?
68
+ determine_next_command_then_inputs_separately
52
69
  else
53
- choose_describe_command_instead
54
- fetch_next_command_class
70
+ determine_next_command_and_inputs
55
71
  end
56
72
 
57
73
  run_next_command
58
- log_command_outcome
74
+ log_last_command_outcome
59
75
  end
60
76
 
61
77
  if given_up
@@ -65,6 +81,10 @@ module Foobara
65
81
  build_result
66
82
  end
67
83
 
84
+ def agent_commands_connected?
85
+ command_connector.agent_commands_connected?
86
+ end
87
+
68
88
  def validate
69
89
  validate_either_command_classes_or_connector_given
70
90
  end
@@ -100,8 +120,7 @@ module Foobara
100
120
  accomplish_goal_command: self,
101
121
  default_serializers: [
102
122
  Foobara::CommandConnectors::Serializers::ErrorsSerializer,
103
- Foobara::CommandConnectors::Serializers::AtomicSerializer,
104
- Foobara::CommandConnectors::Serializers::JsonSerializer
123
+ Foobara::CommandConnectors::Serializers::AtomicSerializer
105
124
  ],
106
125
  llm_model:
107
126
  )
@@ -121,7 +140,132 @@ module Foobara
121
140
  end
122
141
  end
123
142
 
124
- def determine_next_command_name
143
+ def determine_next_command_and_inputs(retries = 2)
144
+ if context.command_log.empty?
145
+ self.next_command_name = ListCommands.full_command_name
146
+ self.next_command_inputs = nil
147
+ fetch_next_command_class
148
+ return
149
+ end
150
+
151
+ inputs_for_determine = {
152
+ goal:,
153
+ context:,
154
+ llm_model:,
155
+ command_class_names: all_command_classes
156
+ }
157
+
158
+ determine_command = DetermineNextCommandNameAndInputs.new(inputs_for_determine)
159
+
160
+ outcome = begin
161
+ determine_command.run
162
+ rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
163
+ Outcome.errors(e.errors)
164
+ end
165
+
166
+ if outcome.success?
167
+ self.next_command_name = outcome.result[:command_name]
168
+ self.next_command_inputs = outcome.result[:inputs]
169
+
170
+ outcome = validate_next_command_name
171
+
172
+ if outcome.success?
173
+ fetch_next_command_class
174
+
175
+ if next_command_has_inputs?
176
+ outcome = validate_next_command_inputs
177
+
178
+ if outcome.success?
179
+ if log_successful_determine_command_and_inputs_outcomes?
180
+ log_command_outcome(
181
+ command: determine_command,
182
+ inputs: determine_command.inputs.except(:context)
183
+ )
184
+ end
185
+ else
186
+ log_command_outcome(
187
+ command: determine_command,
188
+ inputs: determine_command.inputs.except(:context),
189
+ outcome:,
190
+ result: outcome.result || determine_command.raw_result
191
+ )
192
+
193
+ determine_next_command_inputs
194
+ end
195
+ else
196
+ self.next_command_inputs = {}
197
+ end
198
+ else
199
+ log_command_outcome(
200
+ command: determine_command,
201
+ inputs: determine_command.inputs&.except(:context),
202
+ outcome:,
203
+ result: outcome.result || determine_command.raw_result
204
+ )
205
+
206
+ if retries > 0
207
+ determine_next_command_and_inputs(retries - 1)
208
+ else
209
+ determine_next_command_then_inputs_separately
210
+ end
211
+ end
212
+ else
213
+ log_command_outcome(
214
+ command_name: determine_command.class.full_command_name,
215
+ inputs: determine_command.inputs&.except(:context),
216
+ outcome:,
217
+ result: outcome.result || determine_command.raw_result
218
+ )
219
+
220
+ if retries > 0
221
+ determine_next_command_and_inputs(retries - 1)
222
+ else
223
+ determine_next_command_then_inputs_separately
224
+ end
225
+ end
226
+ end
227
+
228
+ def determine_next_command_then_inputs_separately
229
+ determine_next_command_name
230
+
231
+ if command_described?
232
+ fetch_next_command_class
233
+ determine_next_command_inputs
234
+ else
235
+ choose_describe_command_instead
236
+ fetch_next_command_class
237
+ end
238
+ end
239
+
240
+ def validate_next_command_name
241
+ outcome = command_name_type.process_value(next_command_name)
242
+
243
+ if outcome.success?
244
+ self.next_command_name = outcome.result
245
+ end
246
+
247
+ outcome
248
+ end
249
+
250
+ def validate_next_command_inputs
251
+ inputs_type = next_command_class.inputs_type
252
+
253
+ outcome = NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
254
+ inputs_type.process_value(next_command_inputs)
255
+ end
256
+
257
+ if outcome.success?
258
+ self.next_command_inputs = outcome.result
259
+ end
260
+
261
+ outcome
262
+ end
263
+
264
+ def command_name_type
265
+ @command_name_type ||= Agent.foobara_type_from_declaration(:string, one_of: all_command_classes)
266
+ end
267
+
268
+ def determine_next_command_name(retries = 2)
125
269
  self.next_command_name = if context.command_log.empty?
126
270
  ListCommands.full_command_name
127
271
  elsif delayed_command_name
@@ -138,7 +282,39 @@ module Foobara
138
282
  inputs[:llm_model] = llm_model
139
283
  end
140
284
 
141
- command_class.run!(inputs)
285
+ command = command_class.new(inputs)
286
+ outcome = begin
287
+ command.run
288
+ rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
289
+ Outcome.errors(e.errors)
290
+ end
291
+
292
+ if outcome.success?
293
+ if log_successful_determine_command_and_inputs_outcomes?
294
+ log_command_outcome(
295
+ command:,
296
+ inputs: command.inputs.except(:context),
297
+ outcome:
298
+ )
299
+ end
300
+ else
301
+ # TODO: either figure out a way to hit this path in the test suite or delete it
302
+ # :nocov:
303
+ log_command_outcome(
304
+ command:,
305
+ inputs: command.inputs.except(:context),
306
+ outcome:,
307
+ result: outcome.result || command.raw_result
308
+ )
309
+
310
+ if retries > 0
311
+ return determine_next_command_name(retries - 1)
312
+ end
313
+ # :nocov:
314
+ end
315
+
316
+ outcome.raise!
317
+ outcome.result
142
318
  end
143
319
  end
144
320
 
@@ -156,23 +332,61 @@ module Foobara
156
332
  self.next_command_class = command_connector.transformed_command_from_name(next_command_name)
157
333
  end
158
334
 
159
- def determine_next_command_inputs
160
- type = next_command_class.inputs_type
161
-
162
- self.next_command_inputs = if type && !empty_attributes?(type)
163
- command_class = DetermineInputsForNextCommand.for(
164
- command_class: next_command_class, agent_id: agent_name
165
- )
335
+ def determine_next_command_inputs(retries = 2)
336
+ self.next_command_inputs = if next_command_has_inputs?
337
+ command_class = command_class_for_determine_inputs_for_next_command
166
338
 
167
339
  inputs = { goal:, context: }
168
340
  if llm_model
169
341
  inputs[:llm_model] = llm_model
170
342
  end
171
343
 
172
- command_class.run!(inputs)
344
+ command = command_class.new(inputs)
345
+ outcome = begin
346
+ command.run
347
+ rescue CommandPatternImplementation::Concerns::Result::CouldNotProcessResult => e
348
+ Outcome.errors(e.errors)
349
+ end
350
+
351
+ if outcome.success?
352
+ if log_successful_determine_command_and_inputs_outcomes?
353
+ log_command_outcome(
354
+ command:,
355
+ inputs: command.inputs.except(:context),
356
+ outcome:
357
+ )
358
+ end
359
+ else
360
+ # TODO: either figure out a way to hit this path in the test suite or delete it
361
+ # :nocov:
362
+ log_command_outcome(
363
+ command:,
364
+ inputs: command.inputs.except(:context),
365
+ outcome:,
366
+ result: outcome.result || command.raw_result
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
173
376
  end
174
377
  end
175
378
 
379
+ def next_command_has_inputs?
380
+ type = next_command_class.inputs_type
381
+ type && !empty_attributes?(type)
382
+ end
383
+
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
+
176
390
  def run_next_command
177
391
  self.command_response = command_connector.run(
178
392
  full_command_name: next_command_name,
@@ -183,22 +397,46 @@ module Foobara
183
397
  self.command_outcome = command_response.outcome
184
398
  end
185
399
 
186
- def log_command_outcome
187
- outcome_hash = { success: command_outcome.success? }
400
+ def log_last_command_outcome
401
+ log_command_outcome(command: command_response.command)
402
+ end
403
+
404
+ def check_if_too_many_calls
405
+ if context.command_log.size > maximum_command_calls
406
+ add_runtime_error(
407
+ :too_many_command_calls,
408
+ "Too many command calls. " \
409
+ "Stopping. Increase maximum_command_calls if #{maximum_command_calls} is not enough.",
410
+ maximum_command_calls:
411
+ )
412
+ end
413
+ end
188
414
 
189
- if command_outcome.success?
190
- outcome_hash[:result] = command_response.body
191
- else
192
- # :nocov:
193
- outcome_hash[:errors_hash] = command_response.body
194
- # :nocov:
415
+ def log_command_outcome(command: nil, command_name: nil, inputs: nil, outcome: nil, result: nil)
416
+ if command
417
+ command_name ||= command.class.full_command_name
418
+ inputs ||= command.inputs
419
+ outcome ||= command.outcome
420
+ result ||= outcome.result
195
421
  end
196
422
 
197
- context.command_log << CommandLogEntry.new(
198
- command_name: next_command_name,
199
- inputs: next_command_inputs,
423
+ outcome_hash = { success: outcome.success? }
424
+
425
+ if outcome.success? || result
426
+ outcome_hash[:result] = result
427
+ end
428
+
429
+ unless outcome.success?
430
+ outcome_hash[:errors_hash] = outcome.errors_hash
431
+ end
432
+
433
+ log_entry = CommandLogEntry.new(
434
+ command_name:,
435
+ inputs:,
200
436
  outcome: outcome_hash
201
437
  )
438
+
439
+ context.command_log << log_entry
202
440
  end
203
441
 
204
442
  # TODO: these are awkwardly called from outside. Come up with a better solution.
@@ -235,6 +473,14 @@ module Foobara
235
473
  def empty_attributes?(type)
236
474
  type.extends_type?(BuiltinTypes[:attributes]) && type.element_types.empty?
237
475
  end
476
+
477
+ def log_successful_determine_command_and_inputs_outcomes?
478
+ log_successful_determine_command_and_inputs_outcomes
479
+ end
480
+
481
+ def choose_next_command_and_next_inputs_separately?
482
+ choose_next_command_and_next_inputs_separately
483
+ end
238
484
  end
239
485
  end
240
486
  end
@@ -0,0 +1,24 @@
1
+ module Foobara
2
+ class Agent
3
+ module Concerns
4
+ # There's nothing really subclass-specific about this concern, maybe rename it...
5
+ module SubclassCacheable
6
+ def subclass_cache
7
+ @subclass_cache ||= {}
8
+ end
9
+
10
+ def clear_subclass_cache
11
+ @subclass_cache = nil
12
+ end
13
+
14
+ def cached_subclass(key)
15
+ if subclass_cache.key?(key)
16
+ subclass_cache[key]
17
+ else
18
+ subclass_cache[key] = yield
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -36,12 +36,12 @@ module Foobara
36
36
  ]
37
37
 
38
38
  command_classes << if final_result_type
39
- EndSessionBecauseGoalHasBeenAccomplished.for(
39
+ NotifyUserThatCurrentGoalHasBeenAccomplished.for(
40
40
  result_type: final_result_type,
41
41
  agent_id: agent_name
42
42
  )
43
43
  else
44
- EndSessionBecauseGoalHasBeenAccomplished
44
+ NotifyUserThatCurrentGoalHasBeenAccomplished
45
45
  end
46
46
 
47
47
  command_classes.each do |command_class|
@@ -41,13 +41,13 @@ module Foobara
41
41
 
42
42
  def set_inputs_type
43
43
  if command_class.inputs_type
44
- command_description[:inputs_type] = JsonSchemaGenerator.to_json_schema(command_class.inputs_type)
44
+ command_description[:inputs_type] = JSON.parse(JsonSchemaGenerator.to_json_schema(command_class.inputs_type))
45
45
  end
46
46
  end
47
47
 
48
48
  def set_result_type
49
49
  if command_class.result_type
50
- command_description[:result_type] = JsonSchemaGenerator.to_json_schema(command_class.result_type)
50
+ command_description[:result_type] = JSON.parse(JsonSchemaGenerator.to_json_schema(command_class.result_type))
51
51
  end
52
52
  end
53
53
 
@@ -3,41 +3,21 @@ require "foobara/llm_backed_command"
3
3
  module Foobara
4
4
  class Agent
5
5
  class DetermineInputsForNextCommand < Foobara::LlmBackedCommand
6
- class << self
7
- attr_accessor :command_class
8
-
9
- def command_cache
10
- @command_cache ||= {}
11
- end
12
-
13
- def clear_cache
14
- @command_cache = nil
15
- end
16
-
17
- def cached_command(agent_id, full_command_name)
18
- key = [agent_id, full_command_name]
19
-
20
- if command_cache.key?(key)
21
- command_cache[key]
22
- else
23
- command_cache[key] = yield
24
- end
25
- end
6
+ extend Concerns::SubclassCacheable
26
7
 
8
+ class << self
27
9
  def for(command_class:, agent_id:)
28
- cached_command(agent_id, command_class.full_command_name) do
10
+ cached_subclass([command_class.full_command_name, agent_id]) do
29
11
  command_short_name = Util.non_full_name(command_class.command_name)
30
12
  class_name = "Foobara::Agent::#{agent_id}::DetermineInputsForNext#{command_short_name}Command"
31
13
  klass = Util.make_class_p(class_name, self)
32
14
 
33
- klass.command_class = command_class
34
-
35
15
  klass.description "Accepts a goal and context of the work so far and returns the inputs for " \
36
16
  "the next #{command_short_name} command to run to make progress towards " \
37
17
  "accomplishing the goal."
38
18
 
39
19
  klass.inputs do
40
- goal :string, :required, "What do you want the agent to attempt to accomplish?"
20
+ goal :string, :required, "The current (possibly already accomplished) goal"
41
21
  context Context, :required, "Context of the progress towards the goal so far"
42
22
  llm_model :string,
43
23
  one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
@@ -53,23 +33,6 @@ module Foobara
53
33
  end
54
34
  end
55
35
  end
56
-
57
- description "Accepts a goal and context of the work so far and returns the inputs for the next command to " \
58
- "run to make progress towards accomplishing the mission."
59
-
60
- inputs do
61
- goal :string, :required, "What do you want the agent to attempt to accomplish?"
62
- context Context, :required, "Context of the current mission so far"
63
- command_class :duck, :required, "Command to run to accomplish the goal"
64
- llm_model :string,
65
- one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
66
- default: "claude-3-7-sonnet-20250219",
67
- description: "The model to use for the LLM"
68
- end
69
-
70
- result :duck,
71
- description: "Inputs to pass to the next command to run to make progress " \
72
- "towards accomplishing the mission."
73
36
  end
74
37
  end
75
38
  end
@@ -3,35 +3,27 @@ require "foobara/llm_backed_command"
3
3
  module Foobara
4
4
  class Agent
5
5
  class DetermineNextCommand < Foobara::LlmBackedCommand
6
- class << self
7
- attr_accessor :command_class_names
8
-
9
- def command_cache
10
- @command_cache ||= {}
11
- end
12
-
13
- def clear_cache
14
- @command_cache = nil
15
- end
16
-
17
- def cached_command(agent_id)
18
- if command_cache.key?(agent_id)
19
- command_cache[agent_id]
20
- else
21
- command_cache[agent_id] = yield
22
- end
23
- end
6
+ extend Concerns::SubclassCacheable
24
7
 
8
+ class << self
9
+ # Allows us to give a more meaningful result type
25
10
  def for(command_class_names:, agent_id:)
26
- cached_command(agent_id) do
11
+ cached_subclass(agent_id) do
27
12
  command_name = "Foobara::Agent::#{agent_id}::DetermineNextCommand"
28
13
  klass = Util.make_class_p(command_name, self)
29
14
 
30
- klass.command_class_names = command_class_names
15
+ klass.description "Accepts the current goal, which might already be accomplished, " \
16
+ "and context of the work " \
17
+ "so far and returns the name of " \
18
+ "the next command to run to make progress towards " \
19
+ "accomplishing the goal. If the goal has already been accomplished then choose the " \
20
+ "NotifyUserThatCurrentGoalHasBeenAccomplished command."
31
21
 
32
22
  klass.inputs do
33
- goal :string, :required, "What do you want the agent to attempt to accomplish?"
34
- context Context, :required, "Context of the current mission so far"
23
+ goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
24
+ "by the previous command runs then choose " \
25
+ "NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
26
+ context Context, :required, "Context of progress so far"
35
27
  llm_model :string,
36
28
  one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
37
29
  default: "claude-3-7-sonnet-20250219",
@@ -49,19 +41,7 @@ module Foobara
49
41
  end
50
42
 
51
43
  description "Accepts a goal and context of the work so far and returns the name of the next command to run to " \
52
- "make progress towards accomplishing the mission. Make sure you have called DescribeCommand the" \
53
- "command first so that you will know how to construct its inputs in the next step."
54
-
55
- inputs do
56
- goal :string, :required, "What do you want the agent to attempt to accomplish?"
57
- context Context, :required, "Context of the current mission so far"
58
- llm_model :string,
59
- one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
60
- default: "claude-3-7-sonnet-20250219",
61
- description: "The model to use for the LLM"
62
- end
63
-
64
- result :string, description: "Name of the next command to run to make progress towards accomplishing the mission."
44
+ "make progress towards accomplishing the mission."
65
45
  end
66
46
  end
67
47
  end
@@ -0,0 +1,30 @@
1
+ require "foobara/llm_backed_command"
2
+
3
+ module Foobara
4
+ class Agent
5
+ class DetermineNextCommandNameAndInputs < Foobara::LlmBackedCommand
6
+ description "Accepts the current goal, which might already be accomplished, and context of the work " \
7
+ "so far and returns the inputs for " \
8
+ "the next command to run to make progress towards " \
9
+ "accomplishing the goal. If the goal has already been accomplished then choose the " \
10
+ "NotifyUserThatCurrentGoalHasBeenAccomplished command."
11
+
12
+ inputs do
13
+ goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
14
+ "by the previous command runs then choose " \
15
+ "NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
16
+ context Context, :required, "Context of the progress towards the goal so far"
17
+ command_class_names [:string], :required
18
+ llm_model :string,
19
+ one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
20
+ default: "claude-3-7-sonnet-20250219",
21
+ description: "The model to use for the LLM"
22
+ end
23
+
24
+ result do
25
+ command_name :string, :required
26
+ inputs :attributes, :allow_nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,7 +2,7 @@ module Foobara
2
2
  class Agent
3
3
  class GiveUp < Foobara::Command
4
4
  inputs do
5
- command_connector :duck, :required, "Connector to end"
5
+ command_connector CommandConnector, :required, "Connector to end"
6
6
  message_to_user :string, "Optional message to the user explaining why you decided to give up"
7
7
  end
8
8
 
@@ -1,59 +1,48 @@
1
1
  module Foobara
2
2
  class Agent
3
- class EndSessionBecauseGoalHasBeenAccomplished < Foobara::Command
3
+ class NotifyUserThatCurrentGoalHasBeenAccomplished < Foobara::Command
4
+ extend Concerns::SubclassCacheable
5
+
4
6
  class << self
5
7
  attr_accessor :command_class
6
8
 
7
- def command_cache
8
- @command_cache ||= {}
9
- end
10
-
11
- def clear_cache
12
- @command_cache = nil
13
- end
14
-
15
- def cached_command(agent_id, result_type)
16
- key = [agent_id, result_type]
17
-
18
- if command_cache.key?(key)
19
- # :nocov:
20
- command_cache[key]
21
- # :nocov:
22
- else
23
- command_cache[key] = yield
24
- end
25
- end
26
-
27
9
  def for(result_type:, agent_id:)
28
- cached_command(agent_id, result_type) do
29
- command_name = "Foobara::Agent::#{agent_id}::EndSessionBecauseGoalHasBeenAccomplished"
10
+ cached_subclass([result_type, agent_id]) do
11
+ command_name = "Foobara::Agent::#{agent_id}::NotifyUserThatCurrentGoalHasBeenAccomplished"
30
12
  klass = Util.make_class_p(command_name, self)
31
13
 
32
- klass.description "Ends the session giving a final result formatted according to the " \
33
- "result schema if relevant and an optional message to the user."
14
+ klass.description "Notifies the user that the current goal has been accomplished and returns a final " \
15
+ "result formatted according to the " \
16
+ "result schema and an optional message to the user. " \
17
+ "The user might issue a new goal."
34
18
 
35
19
  inputs do
36
- # TODO: Are we still not able to uses classes as foobara types??
37
- command_connector :duck, :required, "Connector to end"
20
+ command_connector CommandConnector, :required, "Connector to notify user through"
38
21
  message_to_user :string, "Optional message to the user"
39
22
  end
40
23
 
41
24
  if result_type
42
25
  add_inputs do
43
- result_data(*result_type)
26
+ result_data result_type
44
27
  end
45
28
 
46
29
  klass.result do
47
30
  message_to_user :string
48
- result_data(*result_type)
31
+ result_data result_type
49
32
  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."
50
37
 
51
- klass.description "Ends the session giving a final result formatted according to the " \
52
- "result schema and an optional message to the user."
53
38
  else
54
39
  # TODO: test this code path
55
40
  # :nocov:
56
- klass.description "Ends the session giving an optional message to the user."
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. " \
45
+ "The user might issue a new goal."
57
46
  # :nocov:
58
47
  end
59
48
 
@@ -3,12 +3,12 @@ module Foobara
3
3
  class CommandLogEntry < Foobara::Model
4
4
  attributes do
5
5
  command_name :string, :required, "Name of the command that was run"
6
- inputs :duck, :required, "Inputs to the command" # TODO: Allow :attributes to be used as a type succesfully
6
+ inputs :attributes, :allow_nil, "Inputs to the command"
7
7
  outcome :required do
8
8
  success :boolean, :required, "Whether the command succeeded or not"
9
- result :duck, :allow_nil, "Result of the command"
9
+ result :duck, "Result of the command"
10
10
  # TODO: create a type for error hash structure
11
- errors_hash :duck, :allow_nil, "Errors that occurred during the command"
11
+ errors_hash :duck, "Errors that occurred during the command"
12
12
  end
13
13
  end
14
14
  end
data/src/foobara/agent.rb CHANGED
@@ -1,21 +1,39 @@
1
- require "io/wait"
2
-
3
1
  module Foobara
4
2
  class Agent
5
- attr_accessor :context, :agent_command_connector, :agent_name, :llm_model
3
+ StateMachine = Foobara::StateMachine.for(
4
+ [:initialized, :idle, :error, :failure] => {
5
+ kill: :killed,
6
+ accomplish_goal: :accomplishing_goal
7
+ },
8
+ accomplishing_goal: {
9
+ goal_accomplished: :idle,
10
+ goal_errored: :error,
11
+ goal_failed: :failure,
12
+ kill: :killed
13
+ }
14
+ )
15
+
16
+ attr_accessor :context,
17
+ :agent_command_connector,
18
+ :agent_name,
19
+ :llm_model,
20
+ :current_accomplish_goal_command,
21
+ :result_type
6
22
 
7
23
  def initialize(
8
24
  context: nil,
9
25
  agent_name: nil,
10
26
  command_classes: nil,
11
27
  agent_command_connector: nil,
12
- llm_model: nil
28
+ llm_model: nil,
29
+ result_type: nil
13
30
  )
14
31
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
15
32
  self.context = context
16
33
  self.agent_command_connector = agent_command_connector
17
34
  self.agent_name = agent_name if agent_name
18
35
  self.llm_model = llm_model
36
+ self.result_type = result_type
19
37
 
20
38
  build_initial_context
21
39
  build_agent_command_connector
@@ -25,20 +43,76 @@ module Foobara
25
43
  end
26
44
  end
27
45
 
28
- def accomplish_goal(goal, result_type: nil)
29
- inputs = {
30
- goal:,
31
- final_result_type: result_type,
32
- current_context: context,
33
- existing_command_connector: agent_command_connector,
34
- agent_name:
35
- }
46
+ def state_machine
47
+ @state_machine ||= StateMachine.new
48
+ end
49
+
50
+ def kill!
51
+ state_machine.perform_transition!(:kill)
52
+ end
53
+
54
+ def killed?
55
+ state_machine.current_state == :killed
56
+ end
36
57
 
37
- if llm_model
38
- inputs[:llm_model] = llm_model
58
+ def accomplish_goal(
59
+ goal,
60
+ result_type: nil,
61
+ choose_next_command_and_next_inputs_separately: nil,
62
+ maximum_call_count: nil
63
+ )
64
+ if result_type && self.result_type != result_type
65
+ if self.result_type
66
+ # :nocov:
67
+ raise ArgumentError, "You can only specify a result type once"
68
+ # :nocov:
69
+ elsif agent_command_connector.agent_commands_connected?
70
+ # :nocov:
71
+ raise ArgumentError, "You can't specify a result type this late in the process"
72
+ # :nocov:
73
+ else
74
+ self.result_type = result_type
75
+ end
39
76
  end
40
77
 
41
- AccomplishGoal.run(inputs)
78
+ state_machine.perform_transition!(:accomplish_goal)
79
+
80
+ begin
81
+ inputs = {
82
+ goal:,
83
+ final_result_type: self.result_type,
84
+ current_context: context,
85
+ existing_command_connector: agent_command_connector,
86
+ agent_name:
87
+ }
88
+
89
+ if llm_model
90
+ inputs[:llm_model] = llm_model
91
+ end
92
+
93
+ unless choose_next_command_and_next_inputs_separately.nil?
94
+ inputs[:choose_next_command_and_next_inputs_separately] = choose_next_command_and_next_inputs_separately
95
+ end
96
+
97
+ unless maximum_call_count.nil?
98
+ inputs[:maximum_command_calls] = maximum_call_count
99
+ end
100
+
101
+ self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
102
+
103
+ current_accomplish_goal_command.run.tap do |outcome|
104
+ if outcome.success?
105
+ state_machine.perform_transition!(:goal_accomplished)
106
+ else
107
+ state_machine.perform_transition!(:goal_errored)
108
+ end
109
+ end
110
+ rescue
111
+ # :nocov:
112
+ state_machine.perform_transition!(:goal_failed)
113
+ raise
114
+ # :nocov:
115
+ end
42
116
  end
43
117
 
44
118
  def build_initial_context
@@ -52,8 +126,7 @@ module Foobara
52
126
  llm_model:,
53
127
  default_serializers: [
54
128
  Foobara::CommandConnectors::Serializers::ErrorsSerializer,
55
- Foobara::CommandConnectors::Serializers::AtomicSerializer,
56
- Foobara::CommandConnectors::Serializers::JsonSerializer
129
+ Foobara::CommandConnectors::Serializers::AtomicSerializer
57
130
  ]
58
131
  )
59
132
  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.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -50,16 +50,18 @@ files:
50
50
  - lib/foobara/agent.rb
51
51
  - src/foobara/agent.rb
52
52
  - src/foobara/agent/accomplish_goal.rb
53
+ - src/foobara/agent/concerns/subclass_cacheable.rb
53
54
  - src/foobara/agent/connector/connector.rb
54
55
  - src/foobara/agent/connector/set_command_connector_inputs_transformer.rb
55
56
  - src/foobara/agent/describe_command.rb
56
57
  - src/foobara/agent/describe_type.rb
57
58
  - src/foobara/agent/determine_inputs_for_next_command.rb
58
59
  - src/foobara/agent/determine_next_command.rb
59
- - src/foobara/agent/end_session_because_goal_has_been_accomplished.rb
60
+ - src/foobara/agent/determine_next_command_name_and_inputs.rb
60
61
  - src/foobara/agent/give_up.rb
61
62
  - src/foobara/agent/list_commands.rb
62
63
  - src/foobara/agent/list_types.rb
64
+ - src/foobara/agent/notify_user_that_current_goal_has_been_accomplished.rb
63
65
  - src/foobara/agent/types/command_log_entry.rb
64
66
  - src/foobara/agent/types/context.rb
65
67
  homepage: https://github.com/foobara/agent