foobara-agent 0.0.1 → 0.0.2
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/lib/foobara/agent.rb +3 -3
- data/src/foobara/agent/accomplish_goal.rb +258 -35
- 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 +3 -40
- data/src/foobara/agent/determine_next_command.rb +6 -35
- data/src/foobara/agent/determine_next_command_name_and_inputs.rb +26 -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 +16 -5
- 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: 7288308d62332addbe98233f7652d718b16f496b02a632426c418642959c5aa4
|
4
|
+
data.tar.gz: 2da140d10bf62e8858de3d5f77c732a0295a4179dfa22e3ddb2c3becf7421029
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 830b3932828ccb30587c64214425f7b4eea862f5deec5c6a9a8d2a8d80733720b66e0d73f936444d91f5dffa6b7f27d8736638c000e17d2caa37cbff477fe6d2
|
7
|
+
data.tar.gz: cd9e89b6dc3dc81f2bd71ed5d6269f7dc8268d950bb120508c8295d1b89b64cd345d79fec8aa2f85cba0945e534e5fc62305ca070f78490f5c3f0303bf72f011
|
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
65
|
until mission_accomplished or given_up or timed_out
|
47
|
-
|
48
|
-
|
49
|
-
if command_described?
|
50
|
-
fetch_next_command_class
|
51
|
-
determine_next_command_inputs
|
66
|
+
if choose_next_command_and_next_inputs_separately?
|
67
|
+
determine_next_command_then_inputs_separately
|
52
68
|
else
|
53
|
-
|
54
|
-
fetch_next_command_class
|
69
|
+
determine_next_command_and_inputs
|
55
70
|
end
|
56
71
|
|
57
72
|
run_next_command
|
58
|
-
|
73
|
+
log_last_command_outcome
|
74
|
+
check_if_too_many_calls
|
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,125 @@ module Foobara
|
|
121
140
|
end
|
122
141
|
end
|
123
142
|
|
124
|
-
def
|
143
|
+
def determine_next_command_and_inputs(retries = 1)
|
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 = determine_command.run
|
161
|
+
|
162
|
+
if outcome.success?
|
163
|
+
self.next_command_name = outcome.result[:command_name]
|
164
|
+
self.next_command_inputs = outcome.result[:inputs]
|
165
|
+
|
166
|
+
outcome = validate_next_command_name
|
167
|
+
|
168
|
+
if outcome.success?
|
169
|
+
fetch_next_command_class
|
170
|
+
|
171
|
+
if next_command_has_inputs?
|
172
|
+
outcome = validate_next_command_inputs
|
173
|
+
|
174
|
+
if outcome.success?
|
175
|
+
if log_successful_determine_command_and_inputs_outcomes?
|
176
|
+
log_command_outcome(
|
177
|
+
command: determine_command,
|
178
|
+
inputs: determine_command.inputs.except(:context)
|
179
|
+
)
|
180
|
+
end
|
181
|
+
else
|
182
|
+
log_command_outcome(
|
183
|
+
command: determine_command,
|
184
|
+
inputs: determine_command.inputs.except(:context),
|
185
|
+
outcome:
|
186
|
+
)
|
187
|
+
|
188
|
+
determine_next_command_inputs
|
189
|
+
end
|
190
|
+
else
|
191
|
+
self.next_command_inputs = {}
|
192
|
+
end
|
193
|
+
else
|
194
|
+
log_command_outcome(
|
195
|
+
command: determine_command,
|
196
|
+
inputs: determine_command.inputs&.except(:context),
|
197
|
+
outcome:
|
198
|
+
)
|
199
|
+
|
200
|
+
if retries > 0
|
201
|
+
determine_next_command_and_inputs(retries - 1)
|
202
|
+
else
|
203
|
+
determine_next_command_then_inputs_separately
|
204
|
+
end
|
205
|
+
end
|
206
|
+
else
|
207
|
+
log_command_outcome(
|
208
|
+
command_name: determine_command.class.full_command_name,
|
209
|
+
inputs: determine_command.inputs&.except(:context),
|
210
|
+
outcome:
|
211
|
+
)
|
212
|
+
|
213
|
+
if retries > 0
|
214
|
+
determine_next_command_and_inputs(retries - 1)
|
215
|
+
else
|
216
|
+
determine_next_command_then_inputs_separately
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def determine_next_command_then_inputs_separately
|
222
|
+
determine_next_command_name
|
223
|
+
|
224
|
+
if command_described?
|
225
|
+
fetch_next_command_class
|
226
|
+
determine_next_command_inputs
|
227
|
+
else
|
228
|
+
choose_describe_command_instead
|
229
|
+
fetch_next_command_class
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def validate_next_command_name
|
234
|
+
outcome = command_name_type.process_value(next_command_name)
|
235
|
+
|
236
|
+
if outcome.success?
|
237
|
+
self.next_command_name = outcome.result
|
238
|
+
end
|
239
|
+
|
240
|
+
outcome
|
241
|
+
end
|
242
|
+
|
243
|
+
def validate_next_command_inputs
|
244
|
+
inputs_type = next_command_class.inputs_type
|
245
|
+
|
246
|
+
outcome = NestedTransactionable.with_needed_transactions_for_type(inputs_type) do
|
247
|
+
inputs_type.process_value(next_command_inputs)
|
248
|
+
end
|
249
|
+
|
250
|
+
if outcome.success?
|
251
|
+
self.next_command_inputs = outcome.result
|
252
|
+
end
|
253
|
+
|
254
|
+
outcome
|
255
|
+
end
|
256
|
+
|
257
|
+
def command_name_type
|
258
|
+
@command_name_type ||= Agent.foobara_type_from_declaration(:string, one_of: all_command_classes)
|
259
|
+
end
|
260
|
+
|
261
|
+
def determine_next_command_name(retries = 1)
|
125
262
|
self.next_command_name = if context.command_log.empty?
|
126
263
|
ListCommands.full_command_name
|
127
264
|
elsif delayed_command_name
|
@@ -138,7 +275,32 @@ module Foobara
|
|
138
275
|
inputs[:llm_model] = llm_model
|
139
276
|
end
|
140
277
|
|
141
|
-
command_class.
|
278
|
+
command = command_class.new(inputs)
|
279
|
+
outcome = command.run
|
280
|
+
|
281
|
+
if outcome.success?
|
282
|
+
if log_successful_determine_command_and_inputs_outcomes?
|
283
|
+
log_command_outcome(
|
284
|
+
command:,
|
285
|
+
inputs: command.inputs.except(:context)
|
286
|
+
)
|
287
|
+
end
|
288
|
+
else
|
289
|
+
# TODO: either figure out a way to hit this path in the test suite or delete it
|
290
|
+
# :nocov:
|
291
|
+
log_command_outcome(
|
292
|
+
command:,
|
293
|
+
inputs: command.inputs.except(:context)
|
294
|
+
)
|
295
|
+
|
296
|
+
if retries > 0
|
297
|
+
return determine_next_command_name(retries - 1)
|
298
|
+
end
|
299
|
+
# :nocov:
|
300
|
+
end
|
301
|
+
|
302
|
+
outcome.raise!
|
303
|
+
outcome.result
|
142
304
|
end
|
143
305
|
end
|
144
306
|
|
@@ -156,23 +318,54 @@ module Foobara
|
|
156
318
|
self.next_command_class = command_connector.transformed_command_from_name(next_command_name)
|
157
319
|
end
|
158
320
|
|
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
|
-
)
|
321
|
+
def determine_next_command_inputs(retries = 1)
|
322
|
+
self.next_command_inputs = if next_command_has_inputs?
|
323
|
+
command_class = command_class_for_determine_inputs_for_next_command
|
166
324
|
|
167
325
|
inputs = { goal:, context: }
|
168
326
|
if llm_model
|
169
327
|
inputs[:llm_model] = llm_model
|
170
328
|
end
|
171
329
|
|
172
|
-
command_class.
|
330
|
+
command = command_class.new(inputs)
|
331
|
+
outcome = command.run
|
332
|
+
|
333
|
+
if outcome.success?
|
334
|
+
if log_successful_determine_command_and_inputs_outcomes?
|
335
|
+
log_command_outcome(
|
336
|
+
command:,
|
337
|
+
inputs: command.inputs.except(:context)
|
338
|
+
)
|
339
|
+
end
|
340
|
+
else
|
341
|
+
# TODO: either figure out a way to hit this path in the test suite or delete it
|
342
|
+
# :nocov:
|
343
|
+
log_command_outcome(
|
344
|
+
command:,
|
345
|
+
inputs: command.inputs.except(:context)
|
346
|
+
)
|
347
|
+
if retries > 0
|
348
|
+
return determine_next_command_inputs(retries - 1)
|
349
|
+
end
|
350
|
+
# :nocov:
|
351
|
+
end
|
352
|
+
|
353
|
+
outcome.raise!
|
354
|
+
outcome.result
|
173
355
|
end
|
174
356
|
end
|
175
357
|
|
358
|
+
def next_command_has_inputs?
|
359
|
+
type = next_command_class.inputs_type
|
360
|
+
type && !empty_attributes?(type)
|
361
|
+
end
|
362
|
+
|
363
|
+
def command_class_for_determine_inputs_for_next_command
|
364
|
+
DetermineInputsForNextCommand.for(
|
365
|
+
command_class: next_command_class, agent_id: agent_name
|
366
|
+
)
|
367
|
+
end
|
368
|
+
|
176
369
|
def run_next_command
|
177
370
|
self.command_response = command_connector.run(
|
178
371
|
full_command_name: next_command_name,
|
@@ -183,20 +376,42 @@ module Foobara
|
|
183
376
|
self.command_outcome = command_response.outcome
|
184
377
|
end
|
185
378
|
|
186
|
-
def
|
187
|
-
|
379
|
+
def log_last_command_outcome
|
380
|
+
log_command_outcome(command: command_response.command)
|
381
|
+
end
|
382
|
+
|
383
|
+
def check_if_too_many_calls
|
384
|
+
if context.command_log.size > maximum_command_calls
|
385
|
+
add_runtime_error(
|
386
|
+
:too_many_command_calls,
|
387
|
+
"Too many command calls. " \
|
388
|
+
"Stopping. Increase maximum_command_calls if #{maximum_command_calls} is not enough.",
|
389
|
+
maximum_command_calls:
|
390
|
+
)
|
391
|
+
end
|
392
|
+
end
|
188
393
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
394
|
+
def log_command_outcome(command: nil, command_name: nil, inputs: nil, outcome: nil, result: nil)
|
395
|
+
if command
|
396
|
+
command_name ||= command.class.full_command_name
|
397
|
+
inputs ||= command.inputs
|
398
|
+
outcome ||= command.outcome
|
399
|
+
result ||= outcome.result
|
400
|
+
end
|
401
|
+
|
402
|
+
outcome_hash = { success: outcome.success? }
|
403
|
+
|
404
|
+
if outcome.success? || result
|
405
|
+
outcome_hash[:result] = result
|
406
|
+
end
|
407
|
+
|
408
|
+
unless outcome.success?
|
409
|
+
outcome_hash[:errors_hash] = outcome.errors_hash
|
195
410
|
end
|
196
411
|
|
197
412
|
context.command_log << CommandLogEntry.new(
|
198
|
-
command_name
|
199
|
-
inputs
|
413
|
+
command_name:,
|
414
|
+
inputs:,
|
200
415
|
outcome: outcome_hash
|
201
416
|
)
|
202
417
|
end
|
@@ -235,6 +450,14 @@ module Foobara
|
|
235
450
|
def empty_attributes?(type)
|
236
451
|
type.extends_type?(BuiltinTypes[:attributes]) && type.element_types.empty?
|
237
452
|
end
|
453
|
+
|
454
|
+
def log_successful_determine_command_and_inputs_outcomes?
|
455
|
+
log_successful_determine_command_and_inputs_outcomes
|
456
|
+
end
|
457
|
+
|
458
|
+
def choose_next_command_and_next_inputs_separately?
|
459
|
+
choose_next_command_and_next_inputs_separately
|
460
|
+
end
|
238
461
|
end
|
239
462
|
end
|
240
463
|
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,35 +3,15 @@ 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."
|
@@ -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,18 @@ 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.command_class_names = command_class_names
|
31
|
-
|
32
15
|
klass.inputs do
|
33
16
|
goal :string, :required, "What do you want the agent to attempt to accomplish?"
|
34
|
-
context Context, :required, "Context of
|
17
|
+
context Context, :required, "Context of progress so far"
|
35
18
|
llm_model :string,
|
36
19
|
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
37
20
|
default: "claude-3-7-sonnet-20250219",
|
@@ -49,19 +32,7 @@ module Foobara
|
|
49
32
|
end
|
50
33
|
|
51
34
|
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."
|
35
|
+
"make progress towards accomplishing the mission."
|
65
36
|
end
|
66
37
|
end
|
67
38
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "foobara/llm_backed_command"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
class Agent
|
5
|
+
class DetermineNextCommandNameAndInputs < Foobara::LlmBackedCommand
|
6
|
+
description "Accepts a goal and context of the work so far and returns the inputs for " \
|
7
|
+
"the next command to run to make progress towards " \
|
8
|
+
"accomplishing the goal."
|
9
|
+
|
10
|
+
inputs do
|
11
|
+
goal :string, :required, "What do you want the agent to attempt to accomplish?"
|
12
|
+
context Context, :required, "Context of the progress towards the goal so far"
|
13
|
+
command_class_names [:string], :required
|
14
|
+
llm_model :string,
|
15
|
+
one_of: Foobara::Ai::AnswerBot::Types::ModelEnum,
|
16
|
+
default: "claude-3-7-sonnet-20250219",
|
17
|
+
description: "The model to use for the LLM"
|
18
|
+
end
|
19
|
+
|
20
|
+
result do
|
21
|
+
command_name :string, :required
|
22
|
+
inputs :attributes, :allow_nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
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,5 +1,3 @@
|
|
1
|
-
require "io/wait"
|
2
|
-
|
3
1
|
module Foobara
|
4
2
|
class Agent
|
5
3
|
attr_accessor :context, :agent_command_connector, :agent_name, :llm_model
|
@@ -25,19 +23,33 @@ module Foobara
|
|
25
23
|
end
|
26
24
|
end
|
27
25
|
|
28
|
-
def accomplish_goal(
|
26
|
+
def accomplish_goal(
|
27
|
+
goal,
|
28
|
+
result_type: nil,
|
29
|
+
choose_next_command_and_next_inputs_separately: nil,
|
30
|
+
maximum_call_count: nil
|
31
|
+
)
|
29
32
|
inputs = {
|
30
33
|
goal:,
|
31
34
|
final_result_type: result_type,
|
32
35
|
current_context: context,
|
33
36
|
existing_command_connector: agent_command_connector,
|
34
37
|
agent_name:
|
38
|
+
|
35
39
|
}
|
36
40
|
|
37
41
|
if llm_model
|
38
42
|
inputs[:llm_model] = llm_model
|
39
43
|
end
|
40
44
|
|
45
|
+
unless choose_next_command_and_next_inputs_separately.nil?
|
46
|
+
inputs[:choose_next_command_and_next_inputs_separately] = choose_next_command_and_next_inputs_separately
|
47
|
+
end
|
48
|
+
|
49
|
+
unless maximum_call_count.nil?
|
50
|
+
inputs[:maximum_command_calls] = maximum_call_count
|
51
|
+
end
|
52
|
+
|
41
53
|
AccomplishGoal.run(inputs)
|
42
54
|
end
|
43
55
|
|
@@ -52,8 +64,7 @@ module Foobara
|
|
52
64
|
llm_model:,
|
53
65
|
default_serializers: [
|
54
66
|
Foobara::CommandConnectors::Serializers::ErrorsSerializer,
|
55
|
-
Foobara::CommandConnectors::Serializers::AtomicSerializer
|
56
|
-
Foobara::CommandConnectors::Serializers::JsonSerializer
|
67
|
+
Foobara::CommandConnectors::Serializers::AtomicSerializer
|
57
68
|
]
|
58
69
|
)
|
59
70
|
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.2
|
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
|