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 +4 -4
- data/CHANGELOG.md +7 -0
- data/src/foobara/agent/accomplish_goal.rb +81 -14
- data/src/foobara/agent/determine_base.rb +82 -0
- data/src/foobara/agent/determine_inputs_for_next_command.rb +11 -23
- data/src/foobara/agent/determine_next_command.rb +5 -24
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +5 -21
- data/src/foobara/agent/types/context.rb +14 -0
- data/src/foobara/agent.rb +9 -6
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c086912a26df48a558f040a3b2e03138210674ec371f528437f08b007ba31783
|
4
|
+
data.tar.gz: 86315598bc4ca17d59cfc6e2d204561c2b387c0162a19601281f3825a7e68337
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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 :
|
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 = {
|
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
|
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 = {
|
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 <
|
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 "
|
16
|
-
"the next #{
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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 <
|
6
|
-
description "
|
7
|
-
"
|
8
|
-
"
|
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
|
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
|
-
|
1
|
+
require_relative "determine_base"
|
2
2
|
|
3
3
|
module Foobara
|
4
4
|
class Agent < CommandConnector
|
5
|
-
class DetermineNextCommandNameAndInputs <
|
6
|
-
description "
|
7
|
-
"
|
8
|
-
"the
|
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
|
-
|
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
|
205
|
-
|
206
|
-
|
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.
|
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
|