foobara-agent 0.0.8 → 0.0.9

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: 12e8facdeb95a000d81ec0ba8a133affa1ea66e470115e978bdc81f05bc51855
4
- data.tar.gz: 294064df166d152c037db13263863b8ccfe32f064a78e77f6f5fe59af0be3829
3
+ metadata.gz: c086912a26df48a558f040a3b2e03138210674ec371f528437f08b007ba31783
4
+ data.tar.gz: 86315598bc4ca17d59cfc6e2d204561c2b387c0162a19601281f3825a7e68337
5
5
  SHA512:
6
- metadata.gz: 2c7fcaf044ba9bc821dcc42f9b0fb44b412fb42f9ef77dd4b7f3ed13cb8293cdb60d448d136e4e0567d104ecb022a00accfd05dbcf955e38185884d34e59668e
7
- data.tar.gz: 380d9ae749b6a942b4e33b112f5aae35fd9600fa9ffcaf462fc0a35122354aae0658378190a4b9db3c0d65d2a12c9d49305b1d03a2881ce504d6d268cf039255
6
+ metadata.gz: 2ce3b72dca0e4e111c8a68f61d9c80eeabbfcfd480d858ebeaa92b9e7cc4730fd4278654ce7b3ded885628297aa0eb8684ee73b7213559e9d9f93760a6c06086
7
+ data.tar.gz: 28c540d5663172ff9d15925f152357872576d96e275bb278edc5c6661c47dfbe9fc0832fec9268fe1b215dfaeb5d43d30eef422bc572dc4e9023e82cd77c3a79
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.0.9] - 2025-06-28
2
+
3
+ - Relocate some common Determine* behavior into a DetermineBase
4
+ - Move goal to Context and make use of LlmBackedCommand#messages
5
+ - Simulating a DescribeCommand selection on failure
6
+ - Compacting the command log
7
+
1
8
  ## [0.0.8] - 2025-06-27
2
9
 
3
10
  - Improve what is logged and its formatting when verbose
@@ -19,7 +19,7 @@ module Foobara
19
19
  io_out :duck
20
20
  io_err :duck
21
21
  agent Agent, :required
22
- current_context Context, :allow_nil, "The current context of the agent"
22
+ context Context, :required, "The current context of the agent"
23
23
  maximum_command_calls :integer,
24
24
  :allow_nil,
25
25
  default: 25,
@@ -46,8 +46,6 @@ module Foobara
46
46
  depends_on ListCommands
47
47
 
48
48
  def execute
49
- build_initial_context_if_necessary
50
-
51
49
  simulate_describe_list_commands_command
52
50
  simulate_list_commands_run
53
51
  # simulate_describe_command_run_for_all_commands
@@ -66,6 +64,7 @@ module Foobara
66
64
 
67
65
  run_next_command
68
66
  log_last_command_outcome
67
+ compact_command_log
69
68
  end
70
69
 
71
70
  if given_up
@@ -75,16 +74,11 @@ module Foobara
75
74
  build_result
76
75
  end
77
76
 
78
- attr_accessor :context, :next_command_name, :next_command_inputs, :next_command_raw_inputs, :mission_accomplished,
77
+ attr_accessor :next_command_name, :next_command_inputs, :next_command_raw_inputs, :mission_accomplished,
79
78
  :given_up, :next_command_class, :next_command, :command_outcome, :timed_out,
80
79
  :final_result, :final_message, :command_response, :delayed_command_name,
81
80
  :command_calls
82
81
 
83
- def build_initial_context_if_necessary
84
- # TODO: shouldn't have to pass command_log here since it has a default, debug that
85
- self.context = current_context || Context.new(command_log: [])
86
- end
87
-
88
82
  def simulate_list_commands_run
89
83
  self.next_command_name = ListCommands.full_command_name
90
84
  self.next_command_raw_inputs = nil
@@ -105,6 +99,26 @@ module Foobara
105
99
  log_last_command_outcome
106
100
  end
107
101
 
102
+ def simulate_describe_command(command_name = next_command_name)
103
+ old_next_command_name = next_command_name
104
+ old_next_command_inputs = next_command_inputs
105
+ old_next_command_raw_inputs = next_command_raw_inputs
106
+ old_next_command_class = next_command_class
107
+
108
+ self.next_command_name = DescribeCommand.full_command_name
109
+ self.next_command_inputs = { command_name: }
110
+ self.next_command_raw_inputs = next_command_inputs
111
+ fetch_next_command_class
112
+
113
+ run_next_command
114
+ log_last_command_outcome
115
+
116
+ self.next_command_name = old_next_command_name
117
+ self.next_command_inputs = old_next_command_inputs
118
+ self.next_command_raw_inputs = old_next_command_raw_inputs
119
+ self.next_command_class = old_next_command_class
120
+ end
121
+
108
122
  def simulate_describe_command_run_for_all_commands
109
123
  # TODO: currently not using this code path. Unclear if it is worth it.
110
124
  # :nocov:
@@ -126,7 +140,6 @@ module Foobara
126
140
 
127
141
  def determine_next_command_and_inputs(retries = 2)
128
142
  inputs_for_determine = {
129
- goal:,
130
143
  context:,
131
144
  llm_model:
132
145
  }
@@ -162,6 +175,7 @@ module Foobara
162
175
  outcome:
163
176
  )
164
177
 
178
+ simulate_describe_command
165
179
  determine_next_command_inputs
166
180
  end
167
181
  else
@@ -251,7 +265,7 @@ module Foobara
251
265
  self.delayed_command_name = nil
252
266
  name
253
267
  else
254
- inputs = { goal:, context: }
268
+ inputs = { context: }
255
269
  if llm_model
256
270
  inputs[:llm_model] = llm_model
257
271
  end
@@ -291,7 +305,7 @@ module Foobara
291
305
  # :nocov:
292
306
  log_command_outcome(
293
307
  command:,
294
- inputs: command.inputs.except(:context),
308
+ inputs: command.inputs&.except(:context),
295
309
  outcome:,
296
310
  result: outcome.result || command.raw_result
297
311
  )
@@ -325,7 +339,7 @@ module Foobara
325
339
  self.next_command_inputs = if next_command_has_inputs?
326
340
  command_class = command_class_for_determine_inputs_for_next_command
327
341
 
328
- inputs = { goal:, context: }
342
+ inputs = { context: }
329
343
  if llm_model
330
344
  inputs[:llm_model] = llm_model
331
345
  end
@@ -349,6 +363,8 @@ module Foobara
349
363
  outcome:
350
364
  )
351
365
 
366
+ simulate_describe_command
367
+
352
368
  if retries > 0
353
369
  return determine_next_command_inputs(retries - 1)
354
370
  end
@@ -412,6 +428,57 @@ module Foobara
412
428
  log_command_outcome(command: command_response.command)
413
429
  end
414
430
 
431
+ def compact_command_log
432
+ # Rules:
433
+ # Delete errors for any command that has succeeded since
434
+ # Delete all but the last DescribeCommand call
435
+ describe_command_call_indexes = []
436
+ commands = {}
437
+
438
+ describe_command_name = DescribeCommand.full_command_name
439
+ context.command_log.each.with_index do |command_log_entry, index|
440
+ command_name = command_log_entry.command_name
441
+
442
+ if command_name == describe_command_name
443
+ describe_command_call_indexes << index
444
+ end
445
+
446
+ commands[command_name] ||= [[], []]
447
+ bucket_index = command_log_entry.outcome[:success] ? 0 : 1
448
+ commands[command_name][bucket_index] << index
449
+ end
450
+
451
+ indexes_to_delete = describe_command_call_indexes[0..-2]
452
+
453
+ commands.each_value do |(success_indexes, failure_indexes)|
454
+ last_success = success_indexes.last
455
+ next unless last_success
456
+
457
+ failure_indexes.each do |failure_index|
458
+ if failure_index < last_success
459
+ indexes_to_delete << failure_index
460
+ else
461
+ # :nocov:
462
+ break
463
+ # :nocov:
464
+ end
465
+ end
466
+ end
467
+
468
+ if indexes_to_delete.empty?
469
+ return
470
+ end
471
+
472
+ new_log = []
473
+ context.command_log = context.command_log.each.with_index do |entry, index|
474
+ unless indexes_to_delete.include?(index)
475
+ new_log << entry
476
+ end
477
+ end
478
+
479
+ context.command_log = new_log
480
+ end
481
+
415
482
  def increment_command_calls
416
483
  self.command_calls ||= -1
417
484
  self.command_calls += 1
@@ -435,7 +502,7 @@ module Foobara
435
502
  outcome ||= command.outcome
436
503
  end
437
504
 
438
- if outcome
505
+ if outcome&.success?
439
506
  result ||= outcome.result
440
507
  end
441
508
 
@@ -0,0 +1,82 @@
1
+ require "foobara/llm_backed_command"
2
+
3
+ module Foobara
4
+ class Agent < CommandConnector
5
+ class DetermineBase < Foobara::LlmBackedCommand
6
+ inputs do
7
+ context Context, :required, "Context of the progress towards the goal so far"
8
+ llm_model :string,
9
+ one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
10
+ default: "claude-3-7-sonnet-20250219",
11
+ description: "The model to use for the LLM"
12
+ end
13
+
14
+ def association_depth
15
+ Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
16
+ end
17
+
18
+ def build_messages
19
+ p = [
20
+ {
21
+ content: llm_instructions,
22
+ role: :system
23
+ }
24
+ ]
25
+
26
+ context.command_log.each do |command_log_entry|
27
+ agent_entry = {
28
+ command: command_log_entry.command_name
29
+ }
30
+
31
+ inputs = command_log_entry.inputs
32
+
33
+ if inputs && !inputs.empty?
34
+ agent_entry[:inputs] = inputs
35
+ end
36
+
37
+ outcome_entry = command_log_entry.outcome
38
+
39
+ p << {
40
+ content: agent_entry,
41
+ role: :assistant
42
+ }
43
+ p << {
44
+ content: outcome_entry,
45
+ role: :user
46
+ }
47
+ end
48
+
49
+ p
50
+ end
51
+
52
+ def llm_instructions
53
+ return @llm_instructions if defined?(@llm_instructions)
54
+
55
+ description = self.class.description
56
+
57
+ instructions = "You are the implementation of a command called #{self.class.scoped_full_name}"
58
+
59
+ instructions += if description && !description.empty?
60
+ " which has the following description:\n\n#{self.class.description}\n\n"
61
+ else
62
+ # :nocov:
63
+ ". "
64
+ # :nocov:
65
+ end
66
+
67
+ instructions += "You are working towards accomplishing the following goal:\n\n#{goal}\n\n"
68
+ instructions += "Your response should match the following JSON schema: \n\n#{self.class.result_json_schema}\n\n"
69
+ instructions += "You can get more details about the result schema for a specific command by " \
70
+ "choosing the DescribeCommand command. " \
71
+ "You will reply with nothing more than the JSON you've generated so that the calling code " \
72
+ "can successfully parse your answer."
73
+
74
+ @llm_instructions = instructions
75
+ end
76
+
77
+ def goal
78
+ context.current_goal
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,8 +1,6 @@
1
- require "foobara/llm_backed_command"
2
-
3
1
  module Foobara
4
2
  class Agent < CommandConnector
5
- class DetermineInputsForNextCommand < Foobara::LlmBackedCommand
3
+ class DetermineInputsForNextCommand < DetermineBase
6
4
  extend Concerns::SubclassCacheable
7
5
 
8
6
  class << self
@@ -12,34 +10,24 @@ module Foobara
12
10
  class_name = "Foobara::Agent::#{agent_id}::DetermineInputsForNext#{command_short_name}Command"
13
11
  klass = Util.make_class_p(class_name, self)
14
12
 
15
- klass.description "Accepts a goal and context of the work so far and returns the inputs for " \
16
- "the next #{command_short_name} command to run to make progress towards " \
17
- "accomplishing the goal."
13
+ klass.description "Returns the inputs for " \
14
+ "the next #{command_class.full_command_name} command to run."
18
15
 
19
- klass.inputs do
20
- goal :string, :required, "The current (possibly already accomplished) goal"
21
- context Context, :required, "Context of the progress towards the goal so far"
22
- llm_model :string,
23
- one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
24
- default: "claude-3-7-sonnet-20250219",
25
- description: "The model to use for the LLM"
16
+ if command_class.inputs_type.nil? || command_class.inputs_type.element_types.empty?
17
+ # :nocov:
18
+ raise ArgumentError, "command #{command_class.full_command_name} has no inputs"
19
+ # :nocov:
26
20
  end
27
21
 
28
- if command_class.inputs_type
29
- transformer = CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer.new(
30
- to: command_class.inputs_type
31
- )
32
- klass.result transformer.from_type
33
- end
22
+ transformer = CommandConnectors::Transformers::EntityToPrimaryKeyInputsTransformer.new(
23
+ to: command_class.inputs_type
24
+ )
25
+ klass.result transformer.from_type
34
26
 
35
27
  klass
36
28
  end
37
29
  end
38
30
  end
39
-
40
- def association_depth
41
- Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
42
- end
43
31
  end
44
32
  end
45
33
  end
@@ -1,32 +1,13 @@
1
- require "foobara/llm_backed_command"
2
-
3
1
  module Foobara
4
2
  class Agent < CommandConnector
5
- class DetermineNextCommand < Foobara::LlmBackedCommand
6
- description "Accepts the current goal, which might already be accomplished, " \
7
- "and context of the work " \
8
- "so far and returns the name of " \
9
- "the next command to run to make progress towards " \
10
- "accomplishing the goal. If the goal has already been accomplished then choose the " \
3
+ class DetermineNextCommand < DetermineBase
4
+ description "Returns the name of the next command to run given the progress " \
5
+ "towards accomplishing the current goal. " \
6
+ "If the goal has been accomplished it will choose the " \
11
7
  "NotifyUserThatCurrentGoalHasBeenAccomplished command."
12
8
 
13
- inputs do
14
- goal :string, :required, "The current goal to accomplish. If the goal has already been accomplished " \
15
- "by the previous command runs then choose " \
16
- "NotifyUserThatCurrentGoalHasBeenAccomplished to stop the loop."
17
- context Context, :required, "Context of progress so far"
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
9
  result :string,
25
- description: "Name of the next command to run to make progress " \
26
- "towards accomplishing the mission"
27
- def association_depth
28
- Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
29
- end
10
+ description: "Name of the next command to run"
30
11
  end
31
12
  end
32
13
  end
@@ -1,33 +1,17 @@
1
- require "foobara/llm_backed_command"
1
+ require_relative "determine_base"
2
2
 
3
3
  module Foobara
4
4
  class Agent < CommandConnector
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 " \
5
+ class DetermineNextCommandNameAndInputs < DetermineBase
6
+ description "Returns the name of the next command to run and its inputs given the progress " \
7
+ "towards accomplishing the current goal. " \
8
+ "If the goal has been accomplished it will choose the " \
10
9
  "NotifyUserThatCurrentGoalHasBeenAccomplished command."
11
10
 
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
- llm_model :string,
18
- one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
19
- default: "claude-3-7-sonnet-20250219",
20
- description: "The model to use for the LLM"
21
- end
22
-
23
11
  result do
24
12
  command_name :string, :required
25
13
  inputs :attributes, :allow_nil
26
14
  end
27
-
28
- def association_depth
29
- Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
30
- end
31
15
  end
32
16
  end
33
17
  end
@@ -1,11 +1,25 @@
1
1
  module Foobara
2
2
  class Agent < CommandConnector
3
3
  class Context < Foobara::Model
4
+ class << self
5
+ def for(goal)
6
+ new(command_log: [], current_goal: goal)
7
+ end
8
+ end
9
+
4
10
  attributes do
11
+ current_goal :string, :required, "The current goal the agent needs to accomplish"
12
+ previous_goals [:string]
5
13
  # TODO: why doesn't this default of [] work as expected on newly created models?
6
14
  command_log [CommandLogEntry], default: [],
7
15
  description: "Log of all commands run so far and their outcomes"
8
16
  end
17
+
18
+ def set_new_goal(goal)
19
+ self.previous_goals ||= []
20
+ previous_goals << current_goal
21
+ self.current_goal = goal
22
+ end
9
23
  end
10
24
  end
11
25
  end
data/src/foobara/agent.rb CHANGED
@@ -67,8 +67,6 @@ module Foobara
67
67
  # TODO: this should work now, switch to this approach
68
68
  # add_default_inputs_transformer EntityToPrimaryKeyInputsTransformer
69
69
 
70
- build_initial_context
71
-
72
70
  # TODO: push this convenience method up into base class?
73
71
  command_classes&.each do |command_class|
74
72
  connect(command_class)
@@ -120,6 +118,8 @@ module Foobara
120
118
  maximum_call_count: nil,
121
119
  llm_model: nil
122
120
  )
121
+ set_context_goal(goal)
122
+
123
123
  if result_type && self.result_type != result_type
124
124
  if self.result_type
125
125
  # :nocov:
@@ -144,7 +144,7 @@ module Foobara
144
144
  inputs = {
145
145
  goal:,
146
146
  final_result_type: self.result_type,
147
- current_context: context,
147
+ context:,
148
148
  agent: self
149
149
  }
150
150
 
@@ -201,9 +201,12 @@ module Foobara
201
201
  end
202
202
  end
203
203
 
204
- def build_initial_context
205
- # TODO: shouldn't have to pass command_log here since it has a default, debug that
206
- self.context ||= Context.new(command_log: [])
204
+ def set_context_goal(goal)
205
+ if context
206
+ context.set_new_goal(goal)
207
+ else
208
+ self.context = Context.for(goal)
209
+ end
207
210
  end
208
211
 
209
212
  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.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -54,6 +54,7 @@ files:
54
54
  - src/foobara/agent/connector/set_command_connector_inputs_transformer.rb
55
55
  - src/foobara/agent/describe_command.rb
56
56
  - src/foobara/agent/describe_type.rb
57
+ - src/foobara/agent/determine_base.rb
57
58
  - src/foobara/agent/determine_inputs_for_next_command.rb
58
59
  - src/foobara/agent/determine_next_command.rb
59
60
  - src/foobara/agent/determine_next_command_name_and_inputs.rb