ruby-agents 0.1.2 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +15 -1
- data/agents.gemspec +8 -11
- data/lib/{actions → agents/actions}/action.rb +2 -5
- data/lib/agents/actions/ask_for_clarification_action.rb +19 -0
- data/lib/agents/actions/delegate_action.rb +20 -0
- data/lib/agents/agents/agent.rb +74 -0
- data/lib/agents/{gpt_agents → agents}/calendar_gpt_agent.rb +1 -1
- data/lib/agents/{gpt_agents → agents}/categorizing_gpt_agent.rb +1 -1
- data/lib/agents/agents/dispatching_gpt_agent.rb +46 -0
- data/lib/agents/{gpt_agents → agents}/information_retrieval_gpt_agent.rb +1 -1
- data/lib/agents/{gpt_agents → agents}/promptless_gpt_agent.rb +1 -1
- data/lib/agents/{gpt_agents → agents}/todo_gpt_agent.rb +1 -1
- data/lib/agents/{unhandleable_request_agent.rb → agents/unhandleable_request_agent.rb} +1 -1
- data/lib/agents/version.rb +3 -0
- data/lib/agents.rb +22 -28
- data/test/test_actions.rb +24 -69
- data/test/test_openai.rb +21 -41
- data/test/test_with_children.rb +75 -0
- metadata +31 -33
- data/lib/agents/agent.rb +0 -24
- 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/lib/version.rb +0 -3
- 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/{actions → agents/actions}/action_argument.rb +0 -0
- /data/lib/{actions → agents/actions}/action_example.rb +0 -0
- /data/lib/agents/{gpt_agents → agents}/echo_gpt_agent.rb +0 -0
- /data/lib/{gpt_clients → agents/gpt_clients}/echo_gpt_client.rb +0 -0
- /data/lib/{gpt_clients → agents/gpt_clients}/gpt_client.rb +0 -0
- /data/lib/{gpt_clients → agents/gpt_clients}/open_ai_gpt_client.rb +0 -0
- /data/lib/{logger.rb → agents/logger.rb} +0 -0
- /data/lib/{requests → agents/requests}/request.rb +0 -0
- /data/lib/{responses → agents/responses}/gpt_response.rb +0 -0
- /data/lib/{responses → agents/responses}/response.rb +0 -0
- /data/lib/{responses → agents/responses}/unhandleable_request_response.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: dea36bf15988aaf769096d19041e9358317ff6a8dc277a418514e0d8e957f8f2
|
4
|
+
data.tar.gz: 183361122b78107dde2646db255871746c5b380ff2c7e483b762c22ccb17d21f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8194908ff998e405a9893e6a663e5b58bd82849230ea753ba04da498eca905539f7db5dd91cf8167093aec59f503685e83d8bc262255c18be3c66af7dbcbc12
|
7
|
+
data.tar.gz: 97f6f490797b0e1c488ce7fda4688893eed2702dcc6ab41f66ea1cef942192764c6457e17d8cec8b82191fdcbf2698a27dfc4f9af41a46edd11596a5f2ec4d8f
|
data/Gemfile.lock
CHANGED
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/agents.gemspec
CHANGED
@@ -1,25 +1,22 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
5
|
-
require_relative 'lib/version'
|
4
|
+
require_relative 'lib/agents/version'
|
6
5
|
|
7
6
|
Gem::Specification.new do |s|
|
8
|
-
s.name
|
7
|
+
s.name = "ruby-agents"
|
9
8
|
s.version = Agents::VERSION
|
10
|
-
s.platform = Gem::Platform::RUBY
|
11
|
-
|
12
9
|
s.authors = ["Jeff McFadden"]
|
13
|
-
s.date = "2023-11-05"
|
14
|
-
s.description = "Put AI to Work."
|
15
10
|
s.email = "55709+jeffmcfadden@users.noreply.github.com"
|
16
|
-
s.files = `git ls-files`.split("\n")
|
17
11
|
|
12
|
+
s.summary = "Setup and organize requests between multiple AI agents."
|
18
13
|
s.homepage = "https://github.com/jeffmcfadden/agents"
|
14
|
+
s.license = "MIT"
|
15
|
+
s.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
19
18
|
s.require_paths = ["lib"]
|
20
|
-
s.rubygems_version = Agents::VERSION
|
21
|
-
s.summary = "Setup and organize requests between multiple AI agents."
|
22
19
|
|
23
20
|
s.add_development_dependency "tldr", ">= 0.9.5"
|
24
21
|
s.add_development_dependency "ruby-openai", ">= 5.1"
|
25
|
-
end
|
22
|
+
end
|
@@ -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
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Agents
|
2
|
+
class Agent
|
3
|
+
attr_reader :gpt_client
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :description
|
6
|
+
attr_reader :system_prompt
|
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
|
29
|
+
|
30
|
+
@actions = actions
|
31
|
+
@actions << Actions::DelegateAction.new(handler: self) if children.any?
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle(request:)
|
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
|
45
|
+
end
|
46
|
+
|
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
|
67
|
+
end
|
68
|
+
|
69
|
+
def delegate_request(request:, args:)
|
70
|
+
children.find{ |c| c.name == args.dig("agent") }&.handle(request: request)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
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
@@ -1,39 +1,33 @@
|
|
1
|
-
require_relative 'logger'
|
2
|
-
require_relative 'version'
|
1
|
+
require_relative 'agents/logger'
|
2
|
+
require_relative 'agents/version'
|
3
3
|
|
4
4
|
require 'openai' rescue nil # OpenAI is optional
|
5
5
|
|
6
|
-
require_relative 'gpt_clients/gpt_client'
|
7
|
-
require_relative 'gpt_clients/echo_gpt_client'
|
6
|
+
require_relative 'agents/gpt_clients/gpt_client'
|
7
|
+
require_relative 'agents/gpt_clients/echo_gpt_client'
|
8
8
|
|
9
9
|
if defined?(::OpenAI::Client)
|
10
|
-
require_relative 'gpt_clients/open_ai_gpt_client'
|
10
|
+
require_relative 'agents/gpt_clients/open_ai_gpt_client'
|
11
11
|
end
|
12
12
|
|
13
|
-
require_relative 'agents/agent'
|
14
|
-
require_relative 'agents/
|
15
|
-
require_relative 'agents/
|
13
|
+
require_relative 'agents/agents/agent'
|
14
|
+
require_relative 'agents/agents/calendar_gpt_agent'
|
15
|
+
require_relative 'agents/agents/categorizing_gpt_agent'
|
16
|
+
require_relative 'agents/agents/dispatching_gpt_agent'
|
17
|
+
require_relative 'agents/agents/information_retrieval_gpt_agent'
|
18
|
+
require_relative 'agents/agents/promptless_gpt_agent'
|
19
|
+
require_relative 'agents/agents/todo_gpt_agent'
|
16
20
|
|
17
|
-
require_relative 'agents/
|
18
|
-
require_relative 'agents/gpt_agents/calendar_gpt_agent'
|
19
|
-
require_relative 'agents/gpt_agents/categorizing_gpt_agent'
|
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'
|
21
|
+
require_relative 'agents/agents/unhandleable_request_agent'
|
24
22
|
|
25
|
-
require_relative 'agents/
|
23
|
+
require_relative 'agents/requests/request'
|
26
24
|
|
27
|
-
require_relative '
|
25
|
+
require_relative 'agents/responses/response'
|
26
|
+
require_relative 'agents/responses/gpt_response'
|
27
|
+
require_relative 'agents/responses/unhandleable_request_response'
|
28
28
|
|
29
|
-
require_relative '
|
30
|
-
require_relative '
|
31
|
-
require_relative '
|
32
|
-
|
33
|
-
require_relative 'actions/
|
34
|
-
require_relative 'actions/action_argument'
|
35
|
-
require_relative 'actions/action_example'
|
36
|
-
|
37
|
-
module Agents
|
38
|
-
|
39
|
-
end
|
29
|
+
require_relative 'agents/actions/action'
|
30
|
+
require_relative 'agents/actions/action_argument'
|
31
|
+
require_relative 'agents/actions/action_example'
|
32
|
+
require_relative 'agents/actions/ask_for_clarification_action'
|
33
|
+
require_relative 'agents/actions/delegate_action'
|
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,14 +1,14 @@
|
|
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff McFadden
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tldr
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '5.1'
|
41
|
-
description:
|
41
|
+
description:
|
42
42
|
email: 55709+jeffmcfadden@users.noreply.github.com
|
43
43
|
executables: []
|
44
44
|
extensions: []
|
@@ -50,38 +50,36 @@ files:
|
|
50
50
|
- LICENSE
|
51
51
|
- README.md
|
52
52
|
- agents.gemspec
|
53
|
-
- lib/actions/action.rb
|
54
|
-
- lib/actions/action_argument.rb
|
55
|
-
- lib/actions/action_example.rb
|
56
53
|
- lib/agents.rb
|
57
|
-
- lib/agents/
|
58
|
-
- lib/agents/
|
59
|
-
- lib/agents/
|
60
|
-
- lib/agents/
|
61
|
-
- lib/agents/
|
62
|
-
- lib/agents/
|
63
|
-
- lib/agents/
|
64
|
-
- lib/agents/
|
65
|
-
- lib/agents/
|
66
|
-
- lib/agents/
|
67
|
-
- lib/agents/
|
68
|
-
- lib/agents/
|
69
|
-
- lib/
|
70
|
-
- lib/
|
71
|
-
- lib/gpt_clients/
|
72
|
-
- lib/
|
73
|
-
- lib/
|
74
|
-
- lib/
|
75
|
-
- lib/
|
76
|
-
- lib/responses/
|
77
|
-
- lib/
|
54
|
+
- lib/agents/actions/action.rb
|
55
|
+
- lib/agents/actions/action_argument.rb
|
56
|
+
- lib/agents/actions/action_example.rb
|
57
|
+
- lib/agents/actions/ask_for_clarification_action.rb
|
58
|
+
- lib/agents/actions/delegate_action.rb
|
59
|
+
- lib/agents/agents/agent.rb
|
60
|
+
- lib/agents/agents/calendar_gpt_agent.rb
|
61
|
+
- lib/agents/agents/categorizing_gpt_agent.rb
|
62
|
+
- lib/agents/agents/dispatching_gpt_agent.rb
|
63
|
+
- lib/agents/agents/echo_gpt_agent.rb
|
64
|
+
- lib/agents/agents/information_retrieval_gpt_agent.rb
|
65
|
+
- lib/agents/agents/promptless_gpt_agent.rb
|
66
|
+
- lib/agents/agents/todo_gpt_agent.rb
|
67
|
+
- lib/agents/agents/unhandleable_request_agent.rb
|
68
|
+
- lib/agents/gpt_clients/echo_gpt_client.rb
|
69
|
+
- lib/agents/gpt_clients/gpt_client.rb
|
70
|
+
- lib/agents/gpt_clients/open_ai_gpt_client.rb
|
71
|
+
- lib/agents/logger.rb
|
72
|
+
- lib/agents/requests/request.rb
|
73
|
+
- lib/agents/responses/gpt_response.rb
|
74
|
+
- lib/agents/responses/response.rb
|
75
|
+
- lib/agents/responses/unhandleable_request_response.rb
|
76
|
+
- lib/agents/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
|
-
licenses:
|
81
|
+
licenses:
|
82
|
+
- MIT
|
85
83
|
metadata: {}
|
86
84
|
post_install_message:
|
87
85
|
rdoc_options: []
|
@@ -91,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
89
|
requirements:
|
92
90
|
- - ">="
|
93
91
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
92
|
+
version: 3.0.0
|
95
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
94
|
requirements:
|
97
95
|
- - ">="
|
98
96
|
- !ruby/object:Gem::Version
|
99
97
|
version: '0'
|
100
98
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
99
|
+
rubygems_version: 3.4.10
|
102
100
|
signing_key:
|
103
101
|
specification_version: 4
|
104
102
|
summary: Setup and organize requests between multiple AI agents.
|
data/lib/agents/agent.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module Agents
|
2
|
-
class Agent
|
3
|
-
attr_reader :actions
|
4
|
-
|
5
|
-
def initialize(actions: [])
|
6
|
-
@actions = actions
|
7
|
-
end
|
8
|
-
|
9
|
-
# @param [Request] request
|
10
|
-
# @return [Response] response
|
11
|
-
def handle(request:)
|
12
|
-
Response.new("Sorry, this agent doesn't know how to handle requests.")
|
13
|
-
end
|
14
|
-
|
15
|
-
def register(action:)
|
16
|
-
@actions << action
|
17
|
-
end
|
18
|
-
|
19
|
-
def unregister(action:)
|
20
|
-
@actions.delete(action)
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
24
|
-
end
|
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/lib/version.rb
DELETED
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|