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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca0396754afd0be4aac5df2f2c120e72d2898eea74f52df8fdfc8c414a941e70
4
- data.tar.gz: 8805783b3594ed85b56658d2a0f8e9386ce6e9cdf1f18c4a9eaa0d12c6f7a7f6
3
+ metadata.gz: cde24c35c39ae9054ab0158bc35ef7af3f891a0c657cbac684da6d9cb42f5744
4
+ data.tar.gz: 65c4a74a3fbcbfd7cb88ddb51e06a98c17789a522699937fa4795fc3ac076be6
5
5
  SHA512:
6
- metadata.gz: 1b08f556416b78e11f9ab6d13f25c3f7f6b7a5c841c859b70d674fe37c91f62f36a3734ee0a388aea0584a5786e495e38b8ab3eb513265a95bfadc46cb1af8db
7
- data.tar.gz: 5dba6e632f9d6cf3c452a796297fbcb191a02296cb0197889bfdc69b28d581f454b74882f47b0a56126ca015b0ce5be2bbb7dd250fa3d316d3af6ca65cd24e47
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', git: "jeffmcfadden/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.
@@ -9,18 +9,15 @@ module Agents
9
9
  # @param [String] description
10
10
  # @param [Array<ActionArgument>] arguments
11
11
  # @param [Array<ActionExample>] examples
12
- # @param [Block] block
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 call(args)
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
- Response.new("Sorry, this agent doesn't know how to handle requests.")
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 register(action:)
16
- @actions << action
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 unregister(action:)
20
- @actions.delete(action)
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
@@ -1,5 +1,5 @@
1
1
  module Agents
2
- class CalendarGptAgent < GptAgent
2
+ class CalendarGptAgent < Agent
3
3
 
4
4
  attr_reader :system_prompt
5
5
  def initialize(**args)
@@ -1,5 +1,5 @@
1
1
  module Agents
2
- class CategorizingGptAgent < GptAgent
2
+ class CategorizingGptAgent < Agent
3
3
 
4
4
  attr_reader :system_prompt
5
5
  def initialize(**args)
@@ -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,5 +1,5 @@
1
1
  module Agents
2
- class InformationRetrievalGptAgent < GptAgent
2
+ class InformationRetrievalGptAgent < Agent
3
3
 
4
4
  attr_reader :system_prompt
5
5
  def initialize(**args)
@@ -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 < GptAgent
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
@@ -1,5 +1,5 @@
1
1
  module Agents
2
- class TodoGptAgent < GptAgent
2
+ class TodoGptAgent < Agent
3
3
 
4
4
  def initialize(**args)
5
5
  super(**args)
@@ -1,5 +1,5 @@
1
1
  module Agents
2
- class UnhandleableRequestAgent
2
+ class UnhandleableRequestAgent < Agent
3
3
  def handle(request:)
4
4
  return UnhandleableRequestResponse.new(request: request)
5
5
  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/dispatcher'
15
- require_relative 'agents/echo_agent'
16
-
17
- require_relative 'agents/gpt_agents/gpt_agent'
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'
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
- module Agents
38
-
39
- end
32
+ require_relative 'actions/ask_for_clarification_action'
33
+ require_relative 'actions/delegate_action'
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Agents
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/test_actions.rb CHANGED
@@ -3,85 +3,40 @@ require 'agents'
3
3
  module Agents
4
4
  class TestActions < TLDR
5
5
 
6
- def setup
7
- @gpt_client = EchoGptClient.new
8
- @dispatcher = DispatchingGptAgent.new(gpt_client: @gpt_client)
9
- @agent = TodoGptAgent.new(gpt_client: @gpt_client)
10
-
11
- @dispatcher.register(@agent)
12
- end
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
- @agent.register(action: test_action)
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
- class TestActionPerformingGptAgent < GptAgent
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 test_performing_action
66
- test_action = TestablePerformingAction.new(name: "test_action") do
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
- # This is a JSON payload because the EchoGPTClient will echo this back as the response
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
- test_request = Request.new(test_payload.to_json)
80
- response = agent.handle(request: test_request)
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 ActionPerformingGptAgent < GptAgent
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
- def test_dispatching_chat
31
- puts "-" * 60
32
- puts "test_dispatching_chat:"
33
- dispatcher = Agents::DispatchingGptAgent.new(gpt_client: @gpt_client)
34
- dispatcher.register(Agents::TodoGptAgent.new(gpt_client: @gpt_client))
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
- response = dispatcher.handle(request: Request.new("I need to buy break and milk"))
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
- dispatcher = Agents::DispatchingGptAgent.new(gpt_client: @gpt_client)
59
- dispatcher.register(todo_agent)
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.1.2
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/dispatcher.rb
59
- - lib/agents/echo_agent.rb
60
- - lib/agents/gpt_agents/calendar_gpt_agent.rb
61
- - lib/agents/gpt_agents/categorizing_gpt_agent.rb
62
- - lib/agents/gpt_agents/dispatching_gpt_agent.rb
63
- - lib/agents/gpt_agents/echo_gpt_agent.rb
64
- - lib/agents/gpt_agents/gpt_agent.rb
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/test_todo_gpt_agent.rb
79
+ - test/test_with_children.rb
83
80
  homepage: https://github.com/jeffmcfadden/agents
84
81
  licenses: []
85
82
  metadata: {}
@@ -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
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Agents
4
- # An agent that echoes back the request text. Useful for testing.
5
- class EchoAgent < Agent
6
-
7
- # @param [Request] request
8
- def handle(request:)
9
- Response.new(request.request_text)
10
- end
11
- end
12
- end
@@ -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
@@ -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
@@ -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
@@ -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