ruby-agents 0.1.2 → 0.2.0
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/README.md +15 -1
- data/lib/actions/action.rb +2 -5
- data/lib/actions/ask_for_clarification_action.rb +19 -0
- data/lib/actions/delegate_action.rb +20 -0
- data/lib/agents/agent.rb +58 -8
- data/lib/agents/{gpt_agents/calendar_gpt_agent.rb → calendar_gpt_agent.rb} +1 -1
- data/lib/agents/{gpt_agents/categorizing_gpt_agent.rb → categorizing_gpt_agent.rb} +1 -1
- data/lib/agents/dispatching_gpt_agent.rb +46 -0
- data/lib/agents/{gpt_agents/information_retrieval_gpt_agent.rb → information_retrieval_gpt_agent.rb} +1 -1
- data/lib/agents/{gpt_agents/promptless_gpt_agent.rb → promptless_gpt_agent.rb} +1 -1
- data/lib/agents/{gpt_agents/todo_gpt_agent.rb → todo_gpt_agent.rb} +1 -1
- data/lib/agents/unhandleable_request_agent.rb +1 -1
- data/lib/agents.rb +8 -14
- data/lib/version.rb +1 -1
- data/test/test_actions.rb +24 -69
- data/test/test_openai.rb +21 -41
- data/test/test_with_children.rb +75 -0
- metadata +11 -14
- data/lib/agents/dispatcher.rb +0 -29
- data/lib/agents/echo_agent.rb +0 -12
- data/lib/agents/gpt_agents/dispatching_gpt_agent.rb +0 -27
- data/lib/agents/gpt_agents/gpt_agent.rb +0 -29
- data/test/test_dispatcher.rb +0 -38
- data/test/test_gpt_dispatcher.rb +0 -25
- data/test/test_todo_gpt_agent.rb +0 -51
- /data/lib/agents/{gpt_agents/echo_gpt_agent.rb → echo_gpt_agent.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cde24c35c39ae9054ab0158bc35ef7af3f891a0c657cbac684da6d9cb42f5744
|
4
|
+
data.tar.gz: 65c4a74a3fbcbfd7cb88ddb51e06a98c17789a522699937fa4795fc3ac076be6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1162c7771022250f190ba3f40ebe0f9fc96b28f0e0033f77ec682b555c0c33f5bde4538c7258ba134ec6e9182d07a7af7bd725c0316b13ddd483da6db551e39
|
7
|
+
data.tar.gz: 8390ffa7fe49f458579e37f12e46e5d1a32d773f8fb80656bda1b92c22474f52698eadf92f164e04302f77b5792bc541218adb7442614d39d06ed3eab22ac755
|
data/README.md
CHANGED
@@ -7,11 +7,22 @@ A ruby library for building and managing AI agents within your application.
|
|
7
7
|
Gemfile:
|
8
8
|
|
9
9
|
```ruby
|
10
|
-
gem 'agents'
|
10
|
+
gem 'ruby-agents'
|
11
11
|
```
|
12
12
|
|
13
13
|
`bundle install`
|
14
14
|
|
15
|
+
### OR
|
16
|
+
|
17
|
+
```shell
|
18
|
+
$ gem install ruby-agents
|
19
|
+
```
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'agents'
|
23
|
+
```
|
24
|
+
|
25
|
+
|
15
26
|
## Usage
|
16
27
|
|
17
28
|
```ruby
|
@@ -27,6 +38,9 @@ gem 'agents', git: "jeffmcfadden/agents"
|
|
27
38
|
### Running Actions
|
28
39
|
|
29
40
|
|
41
|
+
## Prompt Injection
|
42
|
+
At this time there is no built-in protection from prompt injection/jailbreak/etc.
|
43
|
+
|
30
44
|
## Testing
|
31
45
|
|
32
46
|
Use `tldr` to run the tests.
|
data/lib/actions/action.rb
CHANGED
@@ -9,18 +9,15 @@ module Agents
|
|
9
9
|
# @param [String] description
|
10
10
|
# @param [Array<ActionArgument>] arguments
|
11
11
|
# @param [Array<ActionExample>] examples
|
12
|
-
|
13
|
-
def initialize(name:, description: "", arguments: [], examples: [], &block)
|
12
|
+
def initialize(name:, description: "", arguments: [], examples: [])
|
14
13
|
@name = name
|
15
14
|
@description = description
|
16
15
|
@arguments = arguments
|
17
16
|
@examples = examples
|
18
|
-
@block = block
|
19
17
|
end
|
20
18
|
|
21
19
|
# Execute the block with the given arguments.
|
22
|
-
def
|
23
|
-
@block.call(args)
|
20
|
+
def perform(request:, args:)
|
24
21
|
end
|
25
22
|
|
26
23
|
def for_prompt
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agents
|
4
|
+
module Actions
|
5
|
+
class AskForClarificationAction < Action
|
6
|
+
def initialize
|
7
|
+
super(name: "ask_for_clarification",
|
8
|
+
description: "Ask for clarification if the request is not clear, or you don't know how to help.",
|
9
|
+
arguments: [ActionArgument.new(name: "question", type: "String", description: "The question, clarification, or additional information you need.")],
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform(request:, args:)
|
14
|
+
args.dig("question")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Agents
|
2
|
+
module Actions
|
3
|
+
class DelegateAction < Action
|
4
|
+
def initialize(handler:)
|
5
|
+
super(name: "delegate",
|
6
|
+
description: "Delegate to another agent",
|
7
|
+
arguments: [
|
8
|
+
ActionArgument.new(name: "agent", type: "String", description: "The name of the agent to delegate the request to.")
|
9
|
+
]
|
10
|
+
)
|
11
|
+
@handler = handler
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(request:, args:)
|
15
|
+
@handler.delegate_request(request: request, args: args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/agents/agent.rb
CHANGED
@@ -1,23 +1,73 @@
|
|
1
1
|
module Agents
|
2
2
|
class Agent
|
3
|
+
attr_reader :gpt_client
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :description
|
6
|
+
attr_reader :system_prompt
|
3
7
|
attr_reader :actions
|
8
|
+
attr_reader :children
|
9
|
+
|
10
|
+
DEFAULT_SYSTEM_PROMPT = <<~EOS
|
11
|
+
You are a helpful assistant. You will try your best to help answer or delegate each request you receive. If you need
|
12
|
+
additional information to process a request, please ask for it. If you're not quite sure what to do, please ask for
|
13
|
+
help or clarification.
|
14
|
+
|
15
|
+
For each request, please respond ONLY with a JSON object. The JSON object should have a single (one) top-level key: `actions`.
|
16
|
+
The `actions` key should be an array of JSON objects, each of which has a `name` key and an `args` key. If you aren't sure what actions
|
17
|
+
should be taken, or you don't think there's anything relevant, then respond with an empty object for the `actions` key. The `args`
|
18
|
+
key should be a JSON object with the arguments for the action.
|
19
|
+
|
20
|
+
EOS
|
21
|
+
|
22
|
+
# @param actions [Array<Actions::Action>]
|
23
|
+
def initialize(gpt_client:, name: "", description: "", system_prompt: DEFAULT_SYSTEM_PROMPT, actions: [Actions::AskForClarificationAction.new], children: [])
|
24
|
+
@gpt_client = gpt_client
|
25
|
+
@name = name
|
26
|
+
@description = description
|
27
|
+
@system_prompt = system_prompt
|
28
|
+
@children = children
|
4
29
|
|
5
|
-
def initialize(actions: [])
|
6
30
|
@actions = actions
|
31
|
+
@actions << Actions::DelegateAction.new(handler: self) if children.any?
|
7
32
|
end
|
8
33
|
|
9
|
-
# @param [Request] request
|
10
|
-
# @return [Response] response
|
11
34
|
def handle(request:)
|
12
|
-
|
35
|
+
gpt_response = gpt_client.chat system_prompt: build_prompt, prompt: "#{request.request_text}"
|
36
|
+
results = gpt_response.suggested_actions.map{ |sa|
|
37
|
+
actions.find{ |a| a.name == sa.dig("name") }&.perform(request: request, args: sa.dig("args"))
|
38
|
+
}.compact
|
39
|
+
|
40
|
+
if results.empty?
|
41
|
+
"I'm sorry, I don't know how to handle that request."
|
42
|
+
else
|
43
|
+
results.reduce("", :+)
|
44
|
+
end
|
13
45
|
end
|
14
46
|
|
15
|
-
def
|
16
|
-
|
47
|
+
def build_prompt
|
48
|
+
prompt = system_prompt.dup
|
49
|
+
|
50
|
+
if actions.any?
|
51
|
+
prompt << "The ONLY actions you can take are: #{actions.collect(&:name).join(", ")}. Here are the details on those actions: \n"
|
52
|
+
prompt << actions.collect(&:for_prompt).join("\n\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
if children.any?
|
56
|
+
prompt << <<~EOS
|
57
|
+
|
58
|
+
If you don't know how to handle this request, please `delegate` it to another agent. The agents you can `delegate` to are:
|
59
|
+
#{children.each.collect{ " - #{_1.name}: #{_1.description}" }.join("\n")}
|
60
|
+
|
61
|
+
If you choose to delegate this request, please be sure to include `agent` as a key in your `args` object.
|
62
|
+
|
63
|
+
EOS
|
64
|
+
end
|
65
|
+
|
66
|
+
prompt
|
17
67
|
end
|
18
68
|
|
19
|
-
def
|
20
|
-
|
69
|
+
def delegate_request(request:, args:)
|
70
|
+
children.find{ |c| c.name == args.dig("agent") }&.handle(request: request)
|
21
71
|
end
|
22
72
|
|
23
73
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Agents
|
2
|
+
class DispatchingGptAgent < Agent
|
3
|
+
|
4
|
+
attr_reader :agents
|
5
|
+
|
6
|
+
def initialize(agents:[], **args)
|
7
|
+
super(**args)
|
8
|
+
@agents = agents
|
9
|
+
end
|
10
|
+
|
11
|
+
def system_prompt
|
12
|
+
prompt = <<~EOS
|
13
|
+
You are a helpful assistant. You will try your best to help answer or delegate each request you receive. If you need
|
14
|
+
additional information to process a request, please ask for it. If you're not quite sure what to do, please ask for
|
15
|
+
help or clarification.
|
16
|
+
|
17
|
+
For each request, please respond ONLY with a JSON object. The JSON object should have two top-level keys: `response`
|
18
|
+
and `actions`. The `response` key should be a short message that summarizes the actions to be taken. The `actions` key
|
19
|
+
should be an array of JSON objects, each of which has a `name` key and an `args` key. If you aren't sure what actions
|
20
|
+
should be taken, or you don't think there's anything relevant, then respond with an empty array for the `actions` key.
|
21
|
+
|
22
|
+
The actions you can take are:
|
23
|
+
|
24
|
+
`ask_for_clarification`: Ask for help or clarification. The `args` key should be a JSON object with a single key, `request_text`.
|
25
|
+
`delegate`: Delegate the request to another assistant. The `args` key should be a JSON object with a single key, `next_agent`.
|
26
|
+
|
27
|
+
The agents you can delegate to are:
|
28
|
+
- #{agents.each.collect{ " - #{_1.name}: #{_1.description}" }.join("\n")}
|
29
|
+
|
30
|
+
If you are not sure which Assistant to use, it's okay to ask for more information with the `ask_for_clarification` action.
|
31
|
+
|
32
|
+
EOS
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle(request:)
|
36
|
+
response = gpt_client.chat system_prompt: system_prompt, prompt: "#{prompt_prefix}#{request.request_text}"
|
37
|
+
|
38
|
+
response.suggested_actions.each do |action|
|
39
|
+
actions.find{ |a| a.name == action["name"] }&.call(action["args"])
|
40
|
+
end
|
41
|
+
|
42
|
+
response
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Agents
|
2
2
|
# An agent that does not add a prompt to the request, but simply passes it through.
|
3
3
|
# Use if your test _is _the prompt, for example.
|
4
|
-
class PromptlessGptAgent <
|
4
|
+
class PromptlessGptAgent < Agent
|
5
5
|
def handle(request:)
|
6
6
|
Response.new(response_text: gpt_client.chat(prompt: request.request_text))
|
7
7
|
end
|
data/lib/agents.rb
CHANGED
@@ -11,16 +11,12 @@ if defined?(::OpenAI::Client)
|
|
11
11
|
end
|
12
12
|
|
13
13
|
require_relative 'agents/agent'
|
14
|
-
require_relative 'agents/
|
15
|
-
require_relative 'agents/
|
16
|
-
|
17
|
-
require_relative 'agents/
|
18
|
-
require_relative 'agents/
|
19
|
-
require_relative 'agents/
|
20
|
-
require_relative 'agents/gpt_agents/dispatching_gpt_agent'
|
21
|
-
require_relative 'agents/gpt_agents/information_retrieval_gpt_agent'
|
22
|
-
require_relative 'agents/gpt_agents/promptless_gpt_agent'
|
23
|
-
require_relative 'agents/gpt_agents/todo_gpt_agent'
|
14
|
+
require_relative 'agents/calendar_gpt_agent'
|
15
|
+
require_relative 'agents/categorizing_gpt_agent'
|
16
|
+
require_relative 'agents/dispatching_gpt_agent'
|
17
|
+
require_relative 'agents/information_retrieval_gpt_agent'
|
18
|
+
require_relative 'agents/promptless_gpt_agent'
|
19
|
+
require_relative 'agents/todo_gpt_agent'
|
24
20
|
|
25
21
|
require_relative 'agents/unhandleable_request_agent'
|
26
22
|
|
@@ -33,7 +29,5 @@ require_relative 'responses/unhandleable_request_response'
|
|
33
29
|
require_relative 'actions/action'
|
34
30
|
require_relative 'actions/action_argument'
|
35
31
|
require_relative 'actions/action_example'
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
32
|
+
require_relative 'actions/ask_for_clarification_action'
|
33
|
+
require_relative 'actions/delegate_action'
|
data/lib/version.rb
CHANGED
data/test/test_actions.rb
CHANGED
@@ -3,85 +3,40 @@ require 'agents'
|
|
3
3
|
module Agents
|
4
4
|
class TestActions < TLDR
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
class AddTodoAction < Action
|
7
|
+
def initialize
|
8
|
+
super(name: "add_todo",
|
9
|
+
description: "Add a todo to a list",
|
10
|
+
arguments: [ActionArgument.new(name: "todo_list", type: "String", description: "The name of the todo list to add the item to"),
|
11
|
+
ActionArgument.new(name: "item_title", type: "String", description: "The title of the item to add to the list")]
|
12
|
+
)
|
13
13
|
|
14
|
-
def test_dispatching_prompt
|
15
|
-
test_action = Action.new(name: "test_action") do |args|
|
16
|
-
puts "This is a test action"
|
17
14
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# This is a JSON payload because the EchoGPTClient will echo this back as the response
|
22
|
-
test_payload = {
|
23
|
-
"response": "I added peanut butter to your grocery list.",
|
24
|
-
"actions": [
|
25
|
-
{
|
26
|
-
"name": "test_action",
|
27
|
-
"args": [
|
28
|
-
{
|
29
|
-
"todo_list": "Grocery",
|
30
|
-
"item_title": "peanut butter"
|
31
|
-
}
|
32
|
-
]
|
33
|
-
}
|
34
|
-
]
|
35
|
-
}
|
36
|
-
|
37
|
-
test_request = Request.new(test_payload.to_json)
|
38
|
-
|
39
|
-
response = @agent.handle(request: test_request)
|
40
|
-
|
41
|
-
assert_equal "{\"response\":\"I added peanut butter to your grocery list.\",\"actions\":[{\"name\":\"test_action\",\"args\":[{\"todo_list\":\"Grocery\",\"item_title\":\"peanut butter\"}]}]}", response.raw_text
|
42
|
-
# assert_equal "I added peanut butter to your grocery list.", response.response_text
|
43
|
-
# assert_equal response.suggested_actions.size, 1
|
44
|
-
end
|
45
|
-
|
46
|
-
|
47
|
-
class TestablePerformingAction < Action
|
48
|
-
|
49
|
-
attr_reader :was_performed
|
50
|
-
def initialize(**args)
|
51
|
-
super(**args)
|
52
|
-
@was_performed = false
|
53
|
-
end
|
54
|
-
|
55
|
-
def call(args)
|
56
|
-
super(args)
|
57
|
-
@was_performed = true
|
15
|
+
def perform(request:, args:)
|
16
|
+
"I am the add todo action and I just handled a request."
|
58
17
|
end
|
59
18
|
end
|
60
19
|
|
61
|
-
|
20
|
+
def setup
|
21
|
+
@test_gpt_client = Agents::EchoGptClient.new
|
62
22
|
|
23
|
+
@todo_agent = Agent.new(gpt_client: @test_gpt_client,
|
24
|
+
name: "todo_agent",
|
25
|
+
description: "Handles todo list and reminders actions.",
|
26
|
+
actions: [AddTodoAction.new]
|
27
|
+
)
|
63
28
|
end
|
64
29
|
|
65
|
-
def
|
66
|
-
|
67
|
-
return "This is a test action"
|
68
|
-
end
|
69
|
-
|
70
|
-
agent = TestActionPerformingGptAgent.new(gpt_client: @gpt_client, system_prompt: "This is a test prompt", description: "This is a test description")
|
71
|
-
|
72
|
-
assert_equal false, test_action.was_performed
|
73
|
-
|
74
|
-
agent.register(action: test_action)
|
30
|
+
def test_action
|
31
|
+
echo_request_data = { actions: [{ name: "add_todo", args: { todo_list: "Grocery", item_title: "Milk" } }] }
|
75
32
|
|
76
|
-
|
77
|
-
test_payload = { response: "I performed the test action for you", actions: [{ name: "test_action", args: {} }] }
|
33
|
+
response = @todo_agent.handle(request: Request.new(echo_request_data.to_json))
|
78
34
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
assert_equal true, test_action.was_performed
|
83
|
-
assert_equal "I performed the test action for you", response.response_text
|
35
|
+
# We expect this response because the default agent won't know what to do with the request, but it won't have
|
36
|
+
# broken anything either.
|
37
|
+
assert_equal "I am the add todo action and I just handled a request.", response
|
84
38
|
end
|
85
39
|
|
86
40
|
end
|
87
|
-
end
|
41
|
+
end
|
42
|
+
|
data/test/test_openai.rb
CHANGED
@@ -10,56 +10,38 @@ require 'agents'
|
|
10
10
|
|
11
11
|
module Agents
|
12
12
|
|
13
|
-
class
|
13
|
+
class AddTodoAction < Action
|
14
|
+
def initialize
|
15
|
+
super(name: "add_todo",
|
16
|
+
description: "Add a todo to a list",
|
17
|
+
arguments: [ActionArgument.new(name: "todo_list", type: "String", description: "The name of the todo list to add the item to"),
|
18
|
+
ActionArgument.new(name: "item_title", type: "String", description: "The title of the item to add to the list")]
|
19
|
+
)
|
20
|
+
|
21
|
+
end
|
22
|
+
def perform(request:, args:)
|
23
|
+
puts "I am the add todo action and I just handled #{args}"
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
27
|
class TestOpenAi
|
17
28
|
def initialize
|
18
29
|
@openai_client = OpenAI::Client.new(access_token: ENV['OPENAI_ACCESS_TOKEN'])
|
19
30
|
@gpt_client = Agents::OpenAiGptClient.new(open_ai_client: @openai_client)
|
20
|
-
end
|
21
|
-
|
22
|
-
def test_basic_chat
|
23
|
-
puts "-" * 60
|
24
|
-
puts "test_basic_chat:"
|
25
|
-
@agent = Agents::PromptlessGptAgent.new(gpt_client: @gpt_client)
|
26
|
-
response = @agent.handle(request: Request.new("Tell me a joke"))
|
27
|
-
puts response.response_text
|
28
|
-
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
dispatcher.register(Agents::CalendarGptAgent.new(gpt_client: @gpt_client))
|
36
|
-
dispatcher.register(Agents::InformationRetrievalGptAgent.new(gpt_client: @gpt_client))
|
32
|
+
@todo_agent = Agent.new(gpt_client: @gpt_client,
|
33
|
+
name: "todo_agent",
|
34
|
+
description: "Handles todo list and reminders actions.",
|
35
|
+
actions: [AddTodoAction.new]
|
36
|
+
)
|
37
37
|
|
38
|
-
|
39
|
-
puts response
|
38
|
+
@primary_agent = Agent.new(gpt_client: @gpt_client, children: [@todo_agent])
|
40
39
|
end
|
41
40
|
|
42
|
-
def test_todo_action
|
43
|
-
puts "-" * 60
|
44
|
-
puts "test_dispatching_chat:"
|
45
|
-
|
46
|
-
todo_agent = Agents::TodoGptAgent.new(gpt_client: @gpt_client)
|
47
|
-
|
48
|
-
add_todo_action = Action.new(name: "add_todo",
|
49
|
-
description: "Add a todo to a list",
|
50
|
-
arguments: [{name: "todo_list", description: "The name of the todo list to add the item to"},
|
51
|
-
{name: "item_title", description: "The title of the item to add to the list"}]
|
52
|
-
) do |**args|
|
53
|
-
puts "I added the todo"
|
54
|
-
end
|
55
|
-
|
56
|
-
todo_agent.register(action: add_todo_action)
|
57
41
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
response = dispatcher.handle(request: Request.new("I need to buy break and milk"))
|
62
|
-
puts response.response_text
|
42
|
+
def test_todo_action
|
43
|
+
response = @primary_agent.handle(request: Request.new("I need to buy bread and milk"))
|
44
|
+
puts response
|
63
45
|
end
|
64
46
|
|
65
47
|
end
|
@@ -67,7 +49,5 @@ end
|
|
67
49
|
|
68
50
|
# Skip this test if we don't have an access token
|
69
51
|
unless ENV['OPENAI_ACCESS_TOKEN'].nil?
|
70
|
-
# Agents::TestOpenAi.new.test_basic_chat
|
71
|
-
# Agents::TestOpenAi.new.test_dispatching_chat
|
72
52
|
Agents::TestOpenAi.new.test_todo_action
|
73
53
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'agents'
|
2
|
+
|
3
|
+
module Agents
|
4
|
+
class TestWithChildren < TLDR
|
5
|
+
|
6
|
+
class AddTodoAction < Action
|
7
|
+
def initialize
|
8
|
+
super(name: "add_todo",
|
9
|
+
description: "Add a todo to a list",
|
10
|
+
arguments: [ActionArgument.new(name: "todo_list", type: "String", description: "The name of the todo list to add the item to"),
|
11
|
+
ActionArgument.new(name: "item_title", type: "String", description: "The title of the item to add to the list")]
|
12
|
+
)
|
13
|
+
|
14
|
+
end
|
15
|
+
def perform(request:, args:)
|
16
|
+
"I am the add todo action and I just handled a request"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup
|
21
|
+
@test_gpt_client = Agents::EchoGptClient.new
|
22
|
+
|
23
|
+
@todo_agent = Agent.new(gpt_client: @test_gpt_client,
|
24
|
+
name: "todo_agent",
|
25
|
+
description: "Handles todo list and reminders actions.",
|
26
|
+
actions: [AddTodoAction.new]
|
27
|
+
)
|
28
|
+
|
29
|
+
@primary_agent = Agent.new(gpt_client: @test_gpt_client, children: [@todo_agent])
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_prompt
|
33
|
+
expected_prompt = <<~EOS
|
34
|
+
You are a helpful assistant. You will try your best to help answer or delegate each request you receive. If you need
|
35
|
+
additional information to process a request, please ask for it. If you're not quite sure what to do, please ask for
|
36
|
+
help or clarification.
|
37
|
+
|
38
|
+
For each request, please respond ONLY with a JSON object. The JSON object should have a single (one) top-level key: `actions`.
|
39
|
+
The `actions` key should be an array of JSON objects, each of which has a `name` key and an `args` key. If you aren't sure what actions
|
40
|
+
should be taken, or you don't think there's anything relevant, then respond with an empty object for the `actions` key. The `args`
|
41
|
+
key should be a JSON object with the arguments for the action.
|
42
|
+
|
43
|
+
The ONLY actions you can take are: ask_for_clarification, delegate. Here are the details on those actions:
|
44
|
+
ask_for_clarification: Ask for clarification if the request is not clear, or you don't know how to help.
|
45
|
+
Arguments:
|
46
|
+
question: The question, clarification, or additional information you need.
|
47
|
+
|
48
|
+
|
49
|
+
delegate: Delegate to another agent
|
50
|
+
Arguments:
|
51
|
+
agent: The name of the agent to delegate the request to.
|
52
|
+
|
53
|
+
If you don't know how to handle this request, please `delegate` it to another agent. The agents you can `delegate` to are:
|
54
|
+
- todo_agent: Handles todo list and reminders actions.
|
55
|
+
|
56
|
+
If you choose to delegate this request, please be sure to include `agent` as a key in your `args` object.
|
57
|
+
|
58
|
+
EOS
|
59
|
+
|
60
|
+
assert_equal expected_prompt, @primary_agent.build_prompt
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_delegating
|
64
|
+
echo_request_data = { actions: [{ name: "delegate", args: { agent: "todo_agent" } }] }
|
65
|
+
|
66
|
+
response = @primary_agent.handle(request: Request.new(echo_request_data.to_json))
|
67
|
+
|
68
|
+
# We expect this response because the default agent won't know what to do with the request, but it won't have
|
69
|
+
# broken anything either.
|
70
|
+
assert_equal "I'm sorry, I don't know how to handle that request.", response
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-agents
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff McFadden
|
@@ -53,18 +53,17 @@ files:
|
|
53
53
|
- lib/actions/action.rb
|
54
54
|
- lib/actions/action_argument.rb
|
55
55
|
- lib/actions/action_example.rb
|
56
|
+
- lib/actions/ask_for_clarification_action.rb
|
57
|
+
- lib/actions/delegate_action.rb
|
56
58
|
- lib/agents.rb
|
57
59
|
- lib/agents/agent.rb
|
58
|
-
- lib/agents/
|
59
|
-
- lib/agents/
|
60
|
-
- lib/agents/
|
61
|
-
- lib/agents/
|
62
|
-
- lib/agents/
|
63
|
-
- lib/agents/
|
64
|
-
- lib/agents/
|
65
|
-
- lib/agents/gpt_agents/information_retrieval_gpt_agent.rb
|
66
|
-
- lib/agents/gpt_agents/promptless_gpt_agent.rb
|
67
|
-
- lib/agents/gpt_agents/todo_gpt_agent.rb
|
60
|
+
- lib/agents/calendar_gpt_agent.rb
|
61
|
+
- lib/agents/categorizing_gpt_agent.rb
|
62
|
+
- lib/agents/dispatching_gpt_agent.rb
|
63
|
+
- lib/agents/echo_gpt_agent.rb
|
64
|
+
- lib/agents/information_retrieval_gpt_agent.rb
|
65
|
+
- lib/agents/promptless_gpt_agent.rb
|
66
|
+
- lib/agents/todo_gpt_agent.rb
|
68
67
|
- lib/agents/unhandleable_request_agent.rb
|
69
68
|
- lib/gpt_clients/echo_gpt_client.rb
|
70
69
|
- lib/gpt_clients/gpt_client.rb
|
@@ -76,10 +75,8 @@ files:
|
|
76
75
|
- lib/responses/unhandleable_request_response.rb
|
77
76
|
- lib/version.rb
|
78
77
|
- test/test_actions.rb
|
79
|
-
- test/test_dispatcher.rb
|
80
|
-
- test/test_gpt_dispatcher.rb
|
81
78
|
- test/test_openai.rb
|
82
|
-
- test/
|
79
|
+
- test/test_with_children.rb
|
83
80
|
homepage: https://github.com/jeffmcfadden/agents
|
84
81
|
licenses: []
|
85
82
|
metadata: {}
|
data/lib/agents/dispatcher.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module Agents
|
2
|
-
class Dispatcher < Agent
|
3
|
-
attr_reader :agents
|
4
|
-
def initialize(agents: [], **args)
|
5
|
-
super(**args)
|
6
|
-
@agents = agents
|
7
|
-
end
|
8
|
-
|
9
|
-
def register(agent)
|
10
|
-
@agents << agent
|
11
|
-
end
|
12
|
-
|
13
|
-
def unregister(agent)
|
14
|
-
@agents.delete(agent)
|
15
|
-
end
|
16
|
-
|
17
|
-
def handle(request:)
|
18
|
-
agent = agent_for_request(request: request)
|
19
|
-
agent.handle(request: request)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Default behavior is a random agent. Use a subclass to specify behavior.
|
23
|
-
def agent_for_request(request:)
|
24
|
-
return UnhandleableRequestAgent.new if agents.empty?
|
25
|
-
|
26
|
-
@agents.shuffle.first
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/agents/echo_agent.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
module Agents
|
2
|
-
class DispatchingGptAgent < Dispatcher
|
3
|
-
attr_reader :gpt_client
|
4
|
-
|
5
|
-
def initialize(gpt_client:, **args)
|
6
|
-
super(**args)
|
7
|
-
@gpt_client = gpt_client
|
8
|
-
end
|
9
|
-
def agent_for_request(request:)
|
10
|
-
categorize_request = Request.new(prompt_for_categorizing_request(request: request))
|
11
|
-
|
12
|
-
response_data = CategorizingGptAgent.new(gpt_client: gpt_client)
|
13
|
-
.handle(request: categorize_request).data
|
14
|
-
|
15
|
-
agent_class = response_data["category"]
|
16
|
-
agents.find{ _1.class.name == agent_class } || UnhandleableRequestAgent.new
|
17
|
-
end
|
18
|
-
|
19
|
-
def prompt_for_categorizing_request(request:)
|
20
|
-
prompt = "The following is a list of Assistants and a description of what each can do: \n\n"
|
21
|
-
prompt << agents.collect{ "#{_1.class.name}: #{_1.description}" }.join("\n\n")
|
22
|
-
prompt << "If you are not sure which Assistant to use, it's okay to ask for more information.\n\n"
|
23
|
-
prompt << "Please categorize the request by typing the name of the Assistant that best fits the request.\n\n"
|
24
|
-
prompt << "Request: #{request.request_text}"
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Agents
|
2
|
-
class GptAgent < Agent
|
3
|
-
attr_reader :gpt_client
|
4
|
-
attr_reader :description
|
5
|
-
attr_reader :system_prompt
|
6
|
-
|
7
|
-
def initialize(gpt_client:, description: "An AI Chatbot", system_prompt: "", **args)
|
8
|
-
super(**args)
|
9
|
-
@gpt_client = gpt_client
|
10
|
-
@description = description
|
11
|
-
@system_prompt = system_prompt
|
12
|
-
end
|
13
|
-
|
14
|
-
def handle(request:)
|
15
|
-
response = gpt_client.chat system_prompt: system_prompt, prompt: "#{prompt_prefix}#{request.request_text}"
|
16
|
-
|
17
|
-
response.suggested_actions.each do |action|
|
18
|
-
actions.find{ |a| a.name == action["name"] }&.call(action["args"])
|
19
|
-
end
|
20
|
-
|
21
|
-
response
|
22
|
-
end
|
23
|
-
|
24
|
-
def prompt_prefix
|
25
|
-
""
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
data/test/test_dispatcher.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'agents'
|
2
|
-
|
3
|
-
module Agents
|
4
|
-
class TestDispatcher < TLDR
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@dispatcher = Dispatcher.new
|
8
|
-
@agent1 = Agent.new
|
9
|
-
@agent2 = Agent.new
|
10
|
-
|
11
|
-
@echo_agent = EchoAgent.new
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_registration
|
15
|
-
@dispatcher.register(@agent1)
|
16
|
-
@dispatcher.register(@agent2)
|
17
|
-
assert_equal(@dispatcher.agents, [@agent1, @agent2])
|
18
|
-
end
|
19
|
-
|
20
|
-
def test_unregistration
|
21
|
-
@dispatcher.register(@agent1)
|
22
|
-
@dispatcher.register(@agent2)
|
23
|
-
@dispatcher.unregister(@agent1)
|
24
|
-
assert_equal(@dispatcher.agents, [@agent2])
|
25
|
-
end
|
26
|
-
|
27
|
-
def test_default_handling
|
28
|
-
@dispatcher.register(@echo_agent)
|
29
|
-
|
30
|
-
# Default handling is to select a random agent. In this case we have a single
|
31
|
-
# echoing agent, so we expect the codepath to be exercised, but we know it will
|
32
|
-
# always be that one agent.
|
33
|
-
response = @dispatcher.handle(request: Request.new("Test Request"))
|
34
|
-
assert_equal("Test Request", response.response_text )
|
35
|
-
end
|
36
|
-
|
37
|
-
end
|
38
|
-
end
|
data/test/test_gpt_dispatcher.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'agents'
|
2
|
-
|
3
|
-
module Agents
|
4
|
-
class TestGptDispatcher < TLDR
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@gpt_client = EchoGptClient.new
|
8
|
-
@dispatcher = DispatchingGptAgent.new(gpt_client: @gpt_client)
|
9
|
-
@agent1 = TodoGptAgent.new(gpt_client: @gpt_client)
|
10
|
-
@agent2 = CalendarGptAgent.new(gpt_client: @gpt_client)
|
11
|
-
|
12
|
-
@dispatcher.register(@agent1)
|
13
|
-
@dispatcher.register(@agent2)
|
14
|
-
end
|
15
|
-
|
16
|
-
def test_dispatching_prompt
|
17
|
-
request = Request.new("I need to buy some bread")
|
18
|
-
|
19
|
-
expected = "The following is a list of Assistants and a description of what each can do: \n\nAgents::TodoGptAgent: I am an AI Assistant that specializes in Todo List and Reminders Management.\n\nAgents::CalendarGptAgent: I am an AI Assistant that specializes in Calendar and Scheduling Management for Appointments, meetings, etc.Please categorize the request by typing the name of the Assistant that best fits the request.\n\nRequest: I need to buy some bread"
|
20
|
-
|
21
|
-
assert_equal(expected, @dispatcher.prompt_for_categorizing_request(request: request))
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
25
|
-
end
|
data/test/test_todo_gpt_agent.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'agents'
|
2
|
-
|
3
|
-
module Agents
|
4
|
-
class TestTodoGptAgent < TLDR
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@agent = TodoGptAgent.new(gpt_client: EchoGptClient.new)
|
8
|
-
|
9
|
-
add_todo_action = Action.new(name: "add_todo",
|
10
|
-
description: "Add a todo to a list",
|
11
|
-
arguments: [ActionArgument.new(name: "todo_list", type: "String", description: "The name of the todo list to add the item to"),
|
12
|
-
ActionArgument.new(name: "item_title", type: "String", description: "The title of the item to add to the list")],
|
13
|
-
examples: [ActionExample.new(input: "I'm out of milk and bread",
|
14
|
-
output: { response: "I added milk and bread to your grocery list.",
|
15
|
-
actions: [{name: "add_item", args:[{todo_list: "Grocery", "item_title": "milk"}]},
|
16
|
-
{name: "add_item", args:[{todo_list: "Grocery", "item_title": "bread"}]}
|
17
|
-
]}.to_json )]
|
18
|
-
) do |**args|
|
19
|
-
return "I added the todo"
|
20
|
-
end
|
21
|
-
|
22
|
-
@agent.register(action: add_todo_action)
|
23
|
-
end
|
24
|
-
|
25
|
-
def test_prompt
|
26
|
-
expected_prompt = <<~EOS
|
27
|
-
You are a helpful assistant specializing in Todo List and Reminders Management. Please do your best to complete
|
28
|
-
any requests you are given. For each request, please respond ONLY with a JSON object. The JSON object should have
|
29
|
-
two top-level keys: `response` and `actions`. The `response` key should be a short string that summarizes the actions
|
30
|
-
to be taken. The `actions` key should be an array of JSON objects, each of which has a `name` key and an `args` key.
|
31
|
-
If you aren't sure what actions should be taken, or you don't think there's anything relevant, then respond with
|
32
|
-
an empty array for the `actions` key.
|
33
|
-
|
34
|
-
|
35
|
-
The following actions are supported:
|
36
|
-
add_todo: Add a todo to a list
|
37
|
-
Arguments:
|
38
|
-
todo_list: The name of the todo list to add the item to
|
39
|
-
item_title: The title of the item to add to the list
|
40
|
-
For example:
|
41
|
-
Input: I'm out of milk and bread
|
42
|
-
Output: {"response":"I added milk and bread to your grocery list.","actions":[{"name":"add_item","args":[{"todo_list":"Grocery","item_title":"milk"}]},{"name":"add_item","args":[{"todo_list":"Grocery","item_title":"bread"}]}]}
|
43
|
-
|
44
|
-
|
45
|
-
EOS
|
46
|
-
assert_equal(expected_prompt, @agent.system_prompt)
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
end
|
File without changes
|