foobara-agent 0.0.18 → 0.0.20
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 +12 -0
- data/src/foobara/agent/accomplish_goal.rb +66 -7
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +21 -15
- data/src/foobara/agent/notify_user_that_current_goal_has_been_accomplished.rb +56 -33
- data/src/foobara/agent/types/context.rb +5 -7
- data/src/foobara/agent.rb +12 -7
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17773b8b47ace046d920b37565e0b5327fad2bba050f867c9a951ee89b780a93
|
4
|
+
data.tar.gz: 861120d98c2c9cce6f5194403cc950d7ff6c096061be4ebd8cd7a3f93f43c78f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be3168705a2ef7bfcc7a9ab2cde4713c91451857fbb762f121ada60d48cf590d5b0c184ca514221c58fb2b2bdbef08e20f95b7783ba54e83cb9f55fa37546350
|
7
|
+
data.tar.gz: c02c9a0b4dd95b228a8d723ed84af909040f50c8bb2694aea0b30e30a1ab7356f5f074d6a8e64a1ba47b5a113b69e013ad1db0f5fb98ded799ffcc8d5e89760d
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## [0.0.20] - 2025-07-23
|
2
|
+
|
3
|
+
- Fix bug with result type being an array without any message to the user
|
4
|
+
|
5
|
+
## [0.0.19] - 2025-07-23
|
6
|
+
|
7
|
+
- Skip result: input if result is a Model and properly cast result
|
8
|
+
- Add include_message_to_user_in_result option to #accomplish_goal
|
9
|
+
- Include the llm_model in the verbose output
|
10
|
+
- Print out info about retries if verbose
|
11
|
+
- Change the language of the instructions to see what happens
|
12
|
+
|
1
13
|
## [0.0.18] - 2025-07-21
|
2
14
|
|
3
15
|
- Add maximum_command_calls option to Agent#initialize
|
@@ -42,9 +42,8 @@ module Foobara
|
|
42
42
|
simulate_describe_list_commands_command
|
43
43
|
simulate_list_commands_run
|
44
44
|
end
|
45
|
-
# simulate_describe_command_run_for_all_commands
|
46
45
|
|
47
|
-
until mission_accomplished or given_up or killed
|
46
|
+
until mission_accomplished? or given_up? or killed?
|
48
47
|
increment_command_calls
|
49
48
|
check_if_too_many_calls
|
50
49
|
|
@@ -53,7 +52,7 @@ module Foobara
|
|
53
52
|
run_next_command
|
54
53
|
end
|
55
54
|
|
56
|
-
if given_up
|
55
|
+
if given_up?
|
57
56
|
add_given_up_error
|
58
57
|
end
|
59
58
|
|
@@ -124,9 +123,17 @@ module Foobara
|
|
124
123
|
# :nocov:
|
125
124
|
end
|
126
125
|
|
127
|
-
|
126
|
+
RETRY_COUNT = 3
|
127
|
+
|
128
|
+
def determine_next_command_and_inputs(retries = RETRY_COUNT, error_outcome = nil)
|
128
129
|
return if killed
|
129
130
|
|
131
|
+
if verbose? && retries != RETRY_COUNT
|
132
|
+
# :nocov:
|
133
|
+
(io_err || $stderr).puts " !!! Retrying to determine next command and inputs. Retries left: #{retries}"
|
134
|
+
# :nocov:
|
135
|
+
end
|
136
|
+
|
130
137
|
if retries == 0
|
131
138
|
# TODO: test this path by irreparably breaking the needed commands
|
132
139
|
# :nocov:
|
@@ -244,7 +251,8 @@ module Foobara
|
|
244
251
|
inputs_type = next_command_class.inputs_type
|
245
252
|
|
246
253
|
NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
|
247
|
-
|
254
|
+
inputs = next_command_inputs.nil? ? {} : next_command_inputs
|
255
|
+
inputs_type.process_value(inputs)
|
248
256
|
end
|
249
257
|
end
|
250
258
|
|
@@ -425,7 +433,16 @@ module Foobara
|
|
425
433
|
end
|
426
434
|
|
427
435
|
agent_name = agent.agent_name
|
428
|
-
prefix = agent_name && !agent_name.empty?
|
436
|
+
prefix = if agent_name && !agent_name.empty?
|
437
|
+
"#{agent_name}<#{llm_model}>: "
|
438
|
+
else
|
439
|
+
# Not setting an agent_name results in non-determinism in NotifyUser... command
|
440
|
+
# namespace. This results in problems in the test suite making this code path a little
|
441
|
+
# harder to test indirectly.
|
442
|
+
# :nocov:
|
443
|
+
""
|
444
|
+
# :nocov:
|
445
|
+
end
|
429
446
|
|
430
447
|
(io_out || $stdout).puts "#{prefix}#{next_command_name}.run#{args}"
|
431
448
|
end
|
@@ -451,10 +468,40 @@ module Foobara
|
|
451
468
|
add_runtime_error(:gave_up, reason: final_message)
|
452
469
|
end
|
453
470
|
|
471
|
+
def final_result_type_from_declaration
|
472
|
+
return @final_result_type_from_declaration if defined?(@final_result_type_from_declaration)
|
473
|
+
|
474
|
+
@final_result_type_from_declaration = if final_result_type
|
475
|
+
if final_result_type.is_a?(Types::Type)
|
476
|
+
final_result_type
|
477
|
+
else
|
478
|
+
domain = result_type.created_in_namespace.foobara_domain
|
479
|
+
domain.foobara_type_from_declaration(final_result_type)
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
454
484
|
def build_result
|
485
|
+
result_data = if final_result_type_from_declaration
|
486
|
+
outcome = final_result_type_from_declaration.process_value(final_result)
|
487
|
+
|
488
|
+
if outcome.success?
|
489
|
+
outcome.result
|
490
|
+
elsif given_up? || killed?
|
491
|
+
final_result
|
492
|
+
else
|
493
|
+
# :nocov:
|
494
|
+
raise CommandPatternImplementation::Concerns::Result::CouldNotProcessResult,
|
495
|
+
outcome.errors
|
496
|
+
# :nocov:
|
497
|
+
end
|
498
|
+
else
|
499
|
+
final_result
|
500
|
+
end
|
501
|
+
|
455
502
|
{
|
456
503
|
message_to_user: final_message,
|
457
|
-
result_data:
|
504
|
+
result_data:
|
458
505
|
}
|
459
506
|
end
|
460
507
|
|
@@ -497,6 +544,18 @@ module Foobara
|
|
497
544
|
def context
|
498
545
|
agent.context
|
499
546
|
end
|
547
|
+
|
548
|
+
def mission_accomplished?
|
549
|
+
mission_accomplished
|
550
|
+
end
|
551
|
+
|
552
|
+
def given_up?
|
553
|
+
given_up
|
554
|
+
end
|
555
|
+
|
556
|
+
def killed?
|
557
|
+
killed
|
558
|
+
end
|
500
559
|
end
|
501
560
|
end
|
502
561
|
end
|
@@ -17,15 +17,8 @@ module Foobara
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def build_llm_instructions(assistant_association_depth, goal, previous_goals)
|
20
|
-
instructions = "You are
|
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
|
20
|
+
instructions = "You are part of an agent implementation. " \
|
21
|
+
"Your task is to decide which command to run next.\n\n"
|
29
22
|
|
30
23
|
result_schema = result_json_schema(assistant_association_depth)
|
31
24
|
|
@@ -38,13 +31,25 @@ module Foobara
|
|
38
31
|
end
|
39
32
|
end
|
40
33
|
|
41
|
-
instructions += "You are working towards accomplishing the following goal
|
34
|
+
instructions += "You are working towards accomplishing the following goal: #{goal}\n\n"
|
35
|
+
|
36
|
+
instructions += "You will answer with the name of the next command to run and, if needed, its inputs. " \
|
37
|
+
"Choose whichever command is best to make progress towards accomplishing the current goal " \
|
38
|
+
"based on the progress made so far. " \
|
39
|
+
"If the goal has been accomplished then choose the " \
|
40
|
+
"NotifyUserThatCurrentGoalHasBeenAccomplished command. " \
|
41
|
+
"If you are stuck either due to errors " \
|
42
|
+
"or because you do not have the command you need to accomplish the " \
|
43
|
+
"goal, then choose GiveUp.\n\n"
|
42
44
|
|
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
45
|
instructions += "You can get more details about the inputs and result schemas for a specific command by " \
|
46
|
-
"choosing the DescribeCommand command
|
47
|
-
|
46
|
+
"choosing the DescribeCommand command.\n\n"
|
47
|
+
|
48
|
+
instructions += "Your result type is described by the following JSON schema:" \
|
49
|
+
"\n\n#{result_schema}\n\n" \
|
50
|
+
"You will reply with nothing more than the JSON that you've " \
|
51
|
+
"generated that adheres to this result type schema " \
|
52
|
+
"so that the calling code " \
|
48
53
|
"can successfully parse your answer."
|
49
54
|
|
50
55
|
instructions
|
@@ -54,7 +59,8 @@ module Foobara
|
|
54
59
|
description "Returns the name of the next command to run and its inputs given the progress " \
|
55
60
|
"towards accomplishing the current goal. " \
|
56
61
|
"If the goal has been accomplished it will choose the " \
|
57
|
-
"NotifyUserThatCurrentGoalHasBeenAccomplished command."
|
62
|
+
"NotifyUserThatCurrentGoalHasBeenAccomplished command. It will choose GiveUp if it runs into an " \
|
63
|
+
"error it cannot get around or does not believe it has the proper commands to accomplish its goal."
|
58
64
|
|
59
65
|
result do
|
60
66
|
command :string, :required
|
@@ -7,7 +7,7 @@ module Foobara
|
|
7
7
|
|
8
8
|
class << self
|
9
9
|
attr_accessor :command_class, :returns_message_to_user, :returns_result_data, :result_is_attributes,
|
10
|
-
:built_result_type
|
10
|
+
:built_result_type, :result_is_model
|
11
11
|
|
12
12
|
def for(agent_id: nil, result_type: nil, include_message_to_user_in_result: true, result_entity_depth: nil)
|
13
13
|
agent_id ||= "Anon#{SecureRandom.hex(2)}"
|
@@ -46,20 +46,38 @@ module Foobara
|
|
46
46
|
"result schema and a message to the user. " \
|
47
47
|
"The user might issue a new goal."
|
48
48
|
else
|
49
|
-
if result_type.extends?(BuiltinTypes[:
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
49
|
+
if result_type.extends?(BuiltinTypes[:model]) &&
|
50
|
+
!result_type.extends?(BuiltinTypes[:detached_entity]) && result_type != BuiltinTypes[:model]
|
51
|
+
# TODO: Create a ModelToAttributesInputsTransformer in foobara gem and move this logic there
|
52
|
+
model_declaration = result_type.declaration_data
|
53
|
+
|
54
|
+
# Distinguishing between nil and no inputs might be tricky so if :allow_nil then
|
55
|
+
# we will prefer `result: nil` over `nil`
|
56
|
+
# if there's a description we'll also prefer result: so that the description will
|
57
|
+
# make it through to the LLM on that input.
|
58
|
+
# TODO: handle the model's description via the command's description instead.
|
59
|
+
if !model_declaration.key?(:allow_nil) && !model_declaration.key?(:description)
|
60
|
+
klass.built_result_type = result_type
|
61
|
+
klass.result_is_model = true
|
62
|
+
klass.add_inputs result_type.target_class.attributes_type
|
56
63
|
end
|
57
|
-
klass.add_inputs klass.built_result_type
|
58
64
|
end
|
59
65
|
|
60
|
-
klass.
|
61
|
-
|
62
|
-
|
66
|
+
unless klass.result_is_model
|
67
|
+
if result_type.extends?(BuiltinTypes[:attributes])
|
68
|
+
klass.built_result_type = result_type
|
69
|
+
klass.result_is_attributes = true
|
70
|
+
klass.add_inputs result_type
|
71
|
+
else
|
72
|
+
klass.built_result_type = domain.foobara_type_from_declaration do
|
73
|
+
result result_type, :required
|
74
|
+
end
|
75
|
+
klass.add_inputs klass.built_result_type
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
klass.description "Notifies the user that the current goal has been accomplished and accepts a final " \
|
80
|
+
"result value as inputs. " \
|
63
81
|
"The user might issue a new goal."
|
64
82
|
end
|
65
83
|
elsif include_message_to_user_in_result
|
@@ -112,7 +130,7 @@ module Foobara
|
|
112
130
|
DetachedEntity.contains_associations?(inputs_type)
|
113
131
|
command_klass.before_commit_transaction do |command:, **|
|
114
132
|
# TODO: why can't we just pass in the command??
|
115
|
-
built_result = command.
|
133
|
+
built_result = command.final_result_data
|
116
134
|
|
117
135
|
if built_result
|
118
136
|
transformer.process_value!(built_result)
|
@@ -130,34 +148,35 @@ module Foobara
|
|
130
148
|
end
|
131
149
|
|
132
150
|
def execute
|
133
|
-
|
151
|
+
extract_result_data_and_message_to_user
|
134
152
|
mark_mission_accomplished
|
135
153
|
|
136
154
|
nil
|
137
155
|
end
|
138
156
|
|
139
|
-
attr_accessor :
|
140
|
-
|
141
|
-
def
|
142
|
-
result,
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
157
|
+
attr_accessor :final_result_data, :final_message_to_user
|
158
|
+
|
159
|
+
def extract_result_data_and_message_to_user
|
160
|
+
result, message = if returns_message_to_user?
|
161
|
+
inputs.values_at(:result, :message_to_user)
|
162
|
+
elsif returns_result_data?
|
163
|
+
if result_is_attributes?
|
164
|
+
inputs.slice(*self.class.built_result_type.element_types.keys)
|
165
|
+
elsif result_is_model?
|
166
|
+
inputs.slice(
|
167
|
+
*self.class.built_result_type.target_class.attributes_type.element_types.keys
|
168
|
+
)
|
169
|
+
else
|
170
|
+
[inputs[:result], nil]
|
171
|
+
end
|
172
|
+
end
|
147
173
|
|
148
|
-
|
174
|
+
self.final_result_data = result
|
175
|
+
self.final_message_to_user = message
|
149
176
|
end
|
150
177
|
|
151
|
-
def
|
152
|
-
|
153
|
-
inputs.slice(:result, :message_to_user)
|
154
|
-
elsif returns_result_data?
|
155
|
-
if result_is_attributes?
|
156
|
-
inputs.slice(*self.class.built_result_type.element_types.keys)
|
157
|
-
else
|
158
|
-
inputs[:result]
|
159
|
-
end
|
160
|
-
end
|
178
|
+
def mark_mission_accomplished
|
179
|
+
command_connector.mark_mission_accomplished(final_result_data, final_message_to_user)
|
161
180
|
end
|
162
181
|
|
163
182
|
def returns_message_to_user?
|
@@ -171,6 +190,10 @@ module Foobara
|
|
171
190
|
def result_is_attributes?
|
172
191
|
self.class.result_is_attributes
|
173
192
|
end
|
193
|
+
|
194
|
+
def result_is_model?
|
195
|
+
self.class.result_is_model
|
196
|
+
end
|
174
197
|
end
|
175
198
|
end
|
176
199
|
end
|
@@ -3,12 +3,6 @@ require_relative "goal"
|
|
3
3
|
module Foobara
|
4
4
|
class Agent < CommandConnector
|
5
5
|
class Context < Foobara::Model
|
6
|
-
class << self
|
7
|
-
def for(goal)
|
8
|
-
new(command_log: [], current_goal: Goal.new(text: goal))
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
6
|
attributes do
|
13
7
|
current_goal Goal, :required, "The current goal the agent needs to accomplish"
|
14
8
|
previous_goals [Goal]
|
@@ -19,7 +13,11 @@ module Foobara
|
|
19
13
|
|
20
14
|
def set_new_goal(goal)
|
21
15
|
self.previous_goals ||= []
|
22
|
-
|
16
|
+
|
17
|
+
if current_goal
|
18
|
+
previous_goals << current_goal
|
19
|
+
end
|
20
|
+
|
23
21
|
self.current_goal = Goal.new(text: goal)
|
24
22
|
end
|
25
23
|
end
|
data/src/foobara/agent.rb
CHANGED
@@ -44,7 +44,8 @@ module Foobara
|
|
44
44
|
**opts
|
45
45
|
)
|
46
46
|
# TODO: shouldn't have to pass command_log here since it has a default, debug that
|
47
|
-
self.context = context
|
47
|
+
self.context = context || Context.new(command_log: [])
|
48
|
+
|
48
49
|
if agent_name
|
49
50
|
self.agent_name = agent_name
|
50
51
|
end
|
@@ -125,8 +126,13 @@ module Foobara
|
|
125
126
|
goal,
|
126
127
|
result_type: nil,
|
127
128
|
maximum_command_calls: nil,
|
129
|
+
include_message_to_user_in_result: nil,
|
128
130
|
llm_model: nil
|
129
131
|
)
|
132
|
+
unless include_message_to_user_in_result.nil?
|
133
|
+
self.include_message_to_user_in_result = include_message_to_user_in_result
|
134
|
+
end
|
135
|
+
|
130
136
|
set_context_goal(goal)
|
131
137
|
|
132
138
|
if result_type && self.result_type != result_type
|
@@ -150,7 +156,10 @@ module Foobara
|
|
150
156
|
state_machine.perform_transition!(:accomplish_goal)
|
151
157
|
|
152
158
|
begin
|
153
|
-
inputs = accomplish_goal_inputs(goal,
|
159
|
+
inputs = accomplish_goal_inputs(goal,
|
160
|
+
result_type:,
|
161
|
+
maximum_command_calls:,
|
162
|
+
llm_model:)
|
154
163
|
|
155
164
|
self.current_accomplish_goal_command = AccomplishGoal.new(inputs)
|
156
165
|
|
@@ -236,11 +245,7 @@ module Foobara
|
|
236
245
|
end
|
237
246
|
|
238
247
|
def set_context_goal(goal)
|
239
|
-
|
240
|
-
context.set_new_goal(goal)
|
241
|
-
else
|
242
|
-
self.context = Context.for(goal)
|
243
|
-
end
|
248
|
+
context.set_new_goal(goal)
|
244
249
|
end
|
245
250
|
|
246
251
|
def mark_mission_accomplished(final_result, message_to_user)
|