ruby-agents 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|