foobara-agent 0.0.13 → 0.0.16

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: dc4b3804b5930b121fdc679ecf203b9e8b4d11122d24da967cfbfd10a1e6562d
4
- data.tar.gz: '0739391b9b59d453783756a8efed38ec3d559ce7636873776ee71a14027059b5'
3
+ metadata.gz: 240c7fa690a890cd583288e9ed1ae2fb7fe5c70a5322778eccf29ac4bc6554f9
4
+ data.tar.gz: 1d41b1f61ef8126fda960b91231558cf3911fc91a806e005d6c1d36db17d16d6
5
5
  SHA512:
6
- metadata.gz: 0ff04dba39a3edae5a80a98234cd7708c2f90d30bded9f12a0e2b665ea640c302e1ed14c607d61533daa5a0b3d7712684d92de740a3f3e085a1704286f95b837
7
- data.tar.gz: 252b0f1a1c298bd984a856f4e63e57808b63cace046d58cf82f8c55f391c6d21fb3d98b8dda869582aa995eb6438a2fe998bae3952c5f6791eca00d25c5d181c
6
+ metadata.gz: c02671a8ad44659aa2c5aed14bf57f71d3bf14252757302d040e4e2b6060b89c3cbccbc54e35e647cd2c5433a03ef15b12c7dcb97c602fcf4d5edb153d7b2c01
7
+ data.tar.gz: 7b250f7059fb4fda74fff80c4b7c886219abe44b234b48e41a54b954df20a7f549b161cada201b9215d259f17ace85c2ad174f3c95631f45c1ce7c6e965bffa1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.0.16] - 2025-07-09
2
+
3
+ - Tell the llm about previous goals
4
+ - Allow agents to be nameless
5
+ - Improve support for killing a running agent
6
+
7
+ ## [0.0.15] - 2025-07-09
8
+
9
+ - Make use of Ai.default_llm_model
10
+
11
+ ## [0.0.14] - 2025-07-08
12
+
13
+ - Better handle serialization/pre-commit loading for various entity-depth scenarios
14
+
1
15
  ## [0.0.13] - 2025-07-06
2
16
 
3
17
  - Eliminate NotifyUser... result_type to save tokens/improve accuracy
@@ -19,18 +19,19 @@ module Foobara
19
19
  io_out :duck
20
20
  io_err :duck
21
21
  agent Agent, :required
22
- context Context, :required, "The current context of the agent"
23
22
  maximum_command_calls :integer,
24
23
  :allow_nil,
25
24
  default: 25,
26
25
  description: "Maximum number of commands to run before giving up"
27
26
  llm_model :string,
28
27
  :allow_nil,
29
- one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
30
- default: "claude-3-7-sonnet-20250219",
28
+ one_of: Ai::AnswerBot::Types::ModelEnum,
29
+ default: Ai.default_llm_model,
31
30
  description: "The model to use for the LLM"
32
31
  max_llm_calls_per_minute :integer, :allow_nil
33
- result_entity_depth :symbol, :allow_nil, one_of: [:atom, :aggregate]
32
+ user_association_depth :symbol, :allow_nil, one_of: Foobara::AssociationDepth
33
+ result_entity_depth :symbol, :allow_nil, one_of: Foobara::AssociationDepth
34
+ pass_aggregates_to_llm :boolean, :allow_nil
34
35
  end
35
36
 
36
37
  result do
@@ -41,11 +42,13 @@ module Foobara
41
42
  depends_on ListCommands
42
43
 
43
44
  def execute
44
- simulate_describe_list_commands_command
45
- simulate_list_commands_run
45
+ unless list_commands_already_ran?
46
+ simulate_describe_list_commands_command
47
+ simulate_list_commands_run
48
+ end
46
49
  # simulate_describe_command_run_for_all_commands
47
50
 
48
- until mission_accomplished or given_up
51
+ until mission_accomplished or given_up or killed
49
52
  increment_command_calls
50
53
  check_if_too_many_calls
51
54
 
@@ -66,7 +69,11 @@ module Foobara
66
69
  attr_accessor :next_command_name, :next_command_inputs, :next_command_raw_inputs, :mission_accomplished,
67
70
  :given_up, :next_command_class, :next_command, :command_outcome, :timed_out,
68
71
  :final_result, :final_message, :command_response, :delayed_command_name,
69
- :command_calls
72
+ :command_calls, :killed
73
+
74
+ def list_commands_already_ran?
75
+ context.command_log.any? { |log_entry| log_entry.command_name =~ /\bListCommands\z/ }
76
+ end
70
77
 
71
78
  def simulate_list_commands_run
72
79
  self.next_command_name = ListCommands.full_command_name
@@ -124,6 +131,8 @@ module Foobara
124
131
  end
125
132
 
126
133
  def determine_next_command_and_inputs(retries = 3, error_outcome = nil)
134
+ return if killed
135
+
127
136
  if retries == 0
128
137
  # TODO: test this path by irreparably breaking the needed commands
129
138
  # :nocov:
@@ -141,7 +150,10 @@ module Foobara
141
150
 
142
151
  compact_command_log
143
152
 
144
- inputs_for_determine = { context:, llm_model: }
153
+ inputs_for_determine = { agent:, llm_model: }
154
+ unless pass_aggregates_to_llm.nil?
155
+ inputs_for_determine[:pass_aggregates_to_llm] = pass_aggregates_to_llm
156
+ end
145
157
  determine_command = DetermineNextCommandNameAndInputs.new(inputs_for_determine)
146
158
 
147
159
  outcome = begin
@@ -429,6 +441,10 @@ module Foobara
429
441
  self.final_message = message
430
442
  end
431
443
 
444
+ def kill!
445
+ self.killed = true
446
+ end
447
+
432
448
  def give_up!(message)
433
449
  self.given_up = true
434
450
  self.final_message = message
@@ -516,6 +532,10 @@ module Foobara
516
532
  sleep seconds
517
533
  end
518
534
  end
535
+
536
+ def context
537
+ agent.context
538
+ end
519
539
  end
520
540
  end
521
541
  end
@@ -2,17 +2,24 @@ require "foobara/llm_backed_command"
2
2
 
3
3
  module Foobara
4
4
  class Agent < CommandConnector
5
+ # TODO: just move this back to DetermineNextCommandAndInputs since it's now the only base class
5
6
  class DetermineBase < Foobara::LlmBackedCommand
6
7
  inputs do
7
- context Context, :required, "Context of the progress towards the goal so far"
8
+ pass_aggregates_to_llm :boolean, :allow_nil, "Should we send aggregates to the LLM or " \
9
+ "require it to fetch what it needs?"
10
+ agent Agent, :required
8
11
  llm_model :string,
9
12
  one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
10
- default: "claude-3-7-sonnet-20250219",
13
+ default: Ai.default_llm_model,
11
14
  description: "The model to use for the LLM"
12
15
  end
13
16
 
14
17
  def association_depth
15
- Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
18
+ if pass_aggregates_to_llm
19
+ Foobara::AssociationDepth::AGGREGATE
20
+ else
21
+ Foobara::AssociationDepth::ATOM
22
+ end
16
23
  end
17
24
 
18
25
  def build_messages
@@ -49,33 +56,20 @@ module Foobara
49
56
  p
50
57
  end
51
58
 
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."
59
+ def goal
60
+ context.current_goal.text
61
+ end
73
62
 
74
- @llm_instructions = instructions
63
+ def previous_goal_and_status_pairs
64
+ if context.previous_goals && !context.previous_goals.empty?
65
+ context.previous_goals.map do |previous_goal|
66
+ [previous_goal.text, previous_goal.state]
67
+ end
68
+ end
75
69
  end
76
70
 
77
- def goal
78
- context.current_goal
71
+ def context
72
+ agent.context
79
73
  end
80
74
  end
81
75
  end
@@ -3,6 +3,54 @@ require_relative "determine_base"
3
3
  module Foobara
4
4
  class Agent < CommandConnector
5
5
  class DetermineNextCommandNameAndInputs < DetermineBase
6
+ class << self
7
+ def llm_instructions(assistant_association_depth, goal, previous_goals = nil)
8
+ key = [assistant_association_depth, goal, previous_goals]
9
+
10
+ @llm_instructions_cache ||= {}
11
+
12
+ if @llm_instructions_cache.key?(key)
13
+ @llm_instructions_cache[key]
14
+ else
15
+ @llm_instructions_cache[key] = build_llm_instructions(assistant_association_depth, goal, previous_goals)
16
+ end
17
+ end
18
+
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
29
+
30
+ result_schema = result_json_schema(assistant_association_depth)
31
+
32
+ if previous_goals && !previous_goals.empty?
33
+ instructions += "You have previously accomplished the following goals:\n\n"
34
+
35
+ previous_goals.each.with_index do |(previous_goal, state), index|
36
+ instructions += "previous goal #{index + 1}: #{previous_goal}\n"
37
+ instructions += "status of goal #{index + 1}: #{state}\n\n"
38
+ end
39
+ end
40
+
41
+ instructions += "You are working towards accomplishing the following goal:\n\n#{goal}\n\n"
42
+
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
+ 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 " \
48
+ "can successfully parse your answer."
49
+
50
+ instructions
51
+ end
52
+ end
53
+
6
54
  description "Returns the name of the next command to run and its inputs given the progress " \
7
55
  "towards accomplishing the current goal. " \
8
56
  "If the goal has been accomplished it will choose the " \
@@ -12,6 +60,14 @@ module Foobara
12
60
  command :string, :required
13
61
  inputs :attributes, :allow_nil
14
62
  end
63
+
64
+ def determine_llm_instructions
65
+ self.llm_instructions = self.class.llm_instructions(
66
+ computed_user_association_depth,
67
+ goal,
68
+ previous_goal_and_status_pairs
69
+ )
70
+ end
15
71
  end
16
72
  end
17
73
  end
@@ -1,15 +1,17 @@
1
+ require_relative "goal"
2
+
1
3
  module Foobara
2
4
  class Agent < CommandConnector
3
5
  class Context < Foobara::Model
4
6
  class << self
5
7
  def for(goal)
6
- new(command_log: [], current_goal: goal)
8
+ new(command_log: [], current_goal: Goal.new(text: goal))
7
9
  end
8
10
  end
9
11
 
10
12
  attributes do
11
- current_goal :string, :required, "The current goal the agent needs to accomplish"
12
- previous_goals [:string]
13
+ current_goal Goal, :required, "The current goal the agent needs to accomplish"
14
+ previous_goals [Goal]
13
15
  # TODO: why doesn't this default of [] work as expected on newly created models?
14
16
  command_log [CommandLogEntry], default: [],
15
17
  description: "Log of all commands run so far and their outcomes"
@@ -18,7 +20,7 @@ module Foobara
18
20
  def set_new_goal(goal)
19
21
  self.previous_goals ||= []
20
22
  previous_goals << current_goal
21
- self.current_goal = goal
23
+ self.current_goal = Goal.new(text: goal)
22
24
  end
23
25
  end
24
26
  end
@@ -0,0 +1,18 @@
1
+ module Foobara
2
+ class Agent < CommandConnector
3
+ class Goal < Foobara::Model
4
+ module States
5
+ ACCOMPLISHED = :accomplished
6
+ KILLED = :killed
7
+ FAILED = :failed
8
+ ERROR = :error
9
+ GAVE_UP = :gave_up
10
+ end
11
+
12
+ attributes do
13
+ text :string, :required
14
+ state :symbol, :allow_nil, one_of: States
15
+ end
16
+ end
17
+ end
18
+ end
data/src/foobara/agent.rb CHANGED
@@ -15,6 +15,7 @@ module Foobara
15
15
 
16
16
  attr_accessor :context,
17
17
  :agent_name,
18
+ :agent_id,
18
19
  :llm_model,
19
20
  :current_accomplish_goal_command,
20
21
  :result_type,
@@ -24,6 +25,7 @@ module Foobara
24
25
  :io_out,
25
26
  :io_err,
26
27
  :max_llm_calls_per_minute,
28
+ :pass_aggregates_to_llm,
27
29
  :result_entity_depth
28
30
 
29
31
  def initialize(
@@ -38,12 +40,16 @@ module Foobara
38
40
  io_err: nil,
39
41
  max_llm_calls_per_minute: nil,
40
42
  result_entity_depth: AssociationDepth::AGGREGATE,
43
+ pass_aggregates_to_llm: nil,
41
44
  **opts
42
45
  )
43
46
  # TODO: shouldn't have to pass command_log here since it has a default, debug that
44
47
  self.context = context
45
- self.agent_name = agent_name || "Anon#{SecureRandom.hex(2)}"
46
- self.llm_model = llm_model || "claude-opus-4-20250514"
48
+ if agent_name
49
+ self.agent_name = agent_name
50
+ end
51
+ self.agent_id = agent_name || "Anon#{SecureRandom.hex(2)}"
52
+ self.llm_model = llm_model || Ai.default_llm_model
47
53
  self.result_type = result_type
48
54
  self.include_message_to_user_in_result = include_message_to_user_in_result
49
55
  self.verbose = verbose
@@ -51,25 +57,21 @@ module Foobara
51
57
  self.io_err = io_err
52
58
  self.max_llm_calls_per_minute = max_llm_calls_per_minute
53
59
  self.result_entity_depth = result_entity_depth
60
+ self.pass_aggregates_to_llm = pass_aggregates_to_llm
54
61
 
55
- # unless opts.key?(:default_serializers)
56
- # opts = opts.merge(default_serializers: [
57
- # Foobara::CommandConnectors::Serializers::ErrorsSerializer,
58
- # Foobara::CommandConnectors::Serializers::AggregateSerializer
59
- # ])
60
- # end
62
+ pre_commit_transformer = if pass_aggregates_to_llm
63
+ CommandConnectors::Transformers::LoadAggregatesPreCommitTransformer
64
+ else
65
+ CommandConnectors::Transformers::LoadAtomsPreCommitTransformer
66
+ end
61
67
 
62
- unless opts.key?(:default_pre_commit_transformers)
63
- opts = opts.merge(
64
- default_pre_commit_transformers: Foobara::CommandConnectors::Transformers::LoadAtomsPreCommitTransformer
65
- )
66
- end
68
+ opts = opts.merge(default_pre_commit_transformers: [
69
+ *opts[:default_pre_commit_transformers],
70
+ pre_commit_transformer
71
+ ].uniq)
67
72
 
68
73
  super(**opts)
69
74
 
70
- # TODO: this should work now, switch to this approach
71
- # add_default_inputs_transformer EntityToPrimaryKeyInputsTransformer
72
-
73
75
  # TODO: push this convenience method up into base class?
74
76
  command_classes&.each do |command_class|
75
77
  connect(command_class)
@@ -107,11 +109,16 @@ module Foobara
107
109
  end
108
110
 
109
111
  def kill!
110
- state_machine.perform_transition!(:kill)
112
+ current_accomplish_goal_command&.kill!
113
+ state_machine.perform_transition!(:kill) do
114
+ if context
115
+ context.current_goal.state = Goal::States::KILLED
116
+ end
117
+ end
111
118
  end
112
119
 
113
120
  def killed?
114
- state_machine.current_state == :killed
121
+ state_machine.current_state == Goal::States::KILLED
115
122
  end
116
123
 
117
124
  def accomplish_goal(
@@ -151,14 +158,27 @@ module Foobara
151
158
  transition = if outcome.success?
152
159
  :goal_accomplished
153
160
  else
154
- :goal_errored
161
+ :goal_failed
155
162
  end
156
163
 
157
- state_machine.perform_transition!(transition)
164
+ unless killed?
165
+ state_machine.perform_transition!(transition) do
166
+ context.current_goal.state = if outcome.success?
167
+ Goal::States::ACCOMPLISHED
168
+ else
169
+ Goal::States::FAILED
170
+ end
171
+ end
172
+ end
158
173
  end
159
174
  rescue
160
175
  # :nocov:
161
- state_machine.perform_transition!(:goal_failed)
176
+ unless killed?
177
+ state_machine.perform_transition!(:goal_errored) do
178
+ context.current_goal.state = Goal::States::ERROR
179
+ end
180
+ end
181
+
162
182
  raise
163
183
  # :nocov:
164
184
  end
@@ -171,7 +191,6 @@ module Foobara
171
191
  inputs = {
172
192
  goal:,
173
193
  final_result_type: self.result_type,
174
- context:,
175
194
  agent: self
176
195
  }
177
196
 
@@ -209,6 +228,10 @@ module Foobara
209
228
  inputs[:result_entity_depth] = result_entity_depth
210
229
  end
211
230
 
231
+ unless pass_aggregates_to_llm.nil?
232
+ inputs[:pass_aggregates_to_llm] = pass_aggregates_to_llm
233
+ end
234
+
212
235
  inputs
213
236
  end
214
237
 
@@ -246,7 +269,7 @@ module Foobara
246
269
  # TODO: Support changing the final result type when the goal changes
247
270
  NotifyUserThatCurrentGoalHasBeenAccomplished.for(
248
271
  result_type:,
249
- agent_id: agent_name,
272
+ agent_id:,
250
273
  # TODO: Support changing this flag when the goal changes
251
274
  include_message_to_user_in_result:,
252
275
  result_entity_depth:
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.13
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -13,30 +13,42 @@ dependencies:
13
13
  name: foobara
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.0.126
18
+ version: 0.0.136
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: 2.0.0
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 0.0.136
29
+ - - "<"
24
30
  - !ruby/object:Gem::Version
25
- version: 0.0.126
31
+ version: 2.0.0
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: foobara-llm-backed-command
28
34
  requirement: !ruby/object:Gem::Requirement
29
35
  requirements:
30
- - - "~>"
36
+ - - ">="
31
37
  - !ruby/object:Gem::Version
32
- version: 0.0.1
38
+ version: 1.0.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: 2.0.0
33
42
  type: :runtime
34
43
  prerelease: false
35
44
  version_requirements: !ruby/object:Gem::Requirement
36
45
  requirements:
37
- - - "~>"
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 1.0.0
49
+ - - "<"
38
50
  - !ruby/object:Gem::Version
39
- version: 0.0.1
51
+ version: 2.0.0
40
52
  email:
41
53
  - azimux@gmail.com
42
54
  executables: []
@@ -61,6 +73,7 @@ files:
61
73
  - src/foobara/agent/notify_user_that_current_goal_has_been_accomplished.rb
62
74
  - src/foobara/agent/types/command_log_entry.rb
63
75
  - src/foobara/agent/types/context.rb
76
+ - src/foobara/agent/types/goal.rb
64
77
  homepage: https://github.com/foobara/agent
65
78
  licenses:
66
79
  - MPL-2.0