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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/foobara/agent.rb +3 -3
- data/src/foobara/agent/accomplish_goal.rb +283 -37
- data/src/foobara/agent/concerns/subclass_cacheable.rb +24 -0
- data/src/foobara/agent/connector/connector.rb +2 -2
- data/src/foobara/agent/describe_command.rb +2 -2
- data/src/foobara/agent/determine_inputs_for_next_command.rb +4 -41
- data/src/foobara/agent/determine_next_command.rb +15 -35
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +30 -0
- data/src/foobara/agent/give_up.rb +1 -1
- data/src/foobara/agent/{end_session_because_goal_has_been_accomplished.rb → notify_user_that_current_goal_has_been_accomplished.rb} +21 -32
- data/src/foobara/agent/types/command_log_entry.rb +3 -3
- data/src/foobara/agent.rb +90 -17
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88666f0c9353fd88d386752d774b3a5abac7bf035b9349769049a1e633fb6a4e
|
4
|
+
data.tar.gz: 60a09c5cd82d3c4b580ab3224875bc78b3086f04d91e3c28da6708483d4b3806
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
16
|
+
NotifyUserThatCurrentGoalHasBeenAccomplished
|
17
17
|
].each do |command_class|
|
18
|
-
command_class.
|
19
|
-
Util.descendants(command_class).each(&:
|
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 [
|
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 :
|
16
|
-
|
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
|
61
|
+
unless agent_commands_connected?
|
43
62
|
connect_agent_commands
|
44
63
|
end
|
45
64
|
|
46
|
-
until mission_accomplished or given_up
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
54
|
-
fetch_next_command_class
|
70
|
+
determine_next_command_and_inputs
|
55
71
|
end
|
56
72
|
|
57
73
|
run_next_command
|
58
|
-
|
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
|
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.
|
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
|
-
|
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.
|
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
|
187
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
39
|
+
NotifyUserThatCurrentGoalHasBeenAccomplished.for(
|
40
40
|
result_type: final_result_type,
|
41
41
|
agent_id: agent_name
|
42
42
|
)
|
43
43
|
else
|
44
|
-
|
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
|
-
|
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
|
-
|
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, "
|
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
|
-
|
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
|
-
|
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.
|
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, "
|
34
|
-
|
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.
|
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
|
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
|
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
|
-
|
29
|
-
command_name = "Foobara::Agent::#{agent_id}::
|
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 "
|
33
|
-
"result
|
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
|
-
|
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
|
26
|
+
result_data result_type
|
44
27
|
end
|
45
28
|
|
46
29
|
klass.result do
|
47
30
|
message_to_user :string
|
48
|
-
result_data
|
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 "
|
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 :
|
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,
|
9
|
+
result :duck, "Result of the command"
|
10
10
|
# TODO: create a type for error hash structure
|
11
|
-
errors_hash :duck,
|
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
|
-
|
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
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
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.
|
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/
|
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
|