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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +15 -1
  4. data/agents.gemspec +8 -11
  5. data/lib/{actions → agents/actions}/action.rb +2 -5
  6. data/lib/agents/actions/ask_for_clarification_action.rb +19 -0
  7. data/lib/agents/actions/delegate_action.rb +20 -0
  8. data/lib/agents/agents/agent.rb +74 -0
  9. data/lib/agents/{gpt_agents → agents}/calendar_gpt_agent.rb +1 -1
  10. data/lib/agents/{gpt_agents → agents}/categorizing_gpt_agent.rb +1 -1
  11. data/lib/agents/agents/dispatching_gpt_agent.rb +46 -0
  12. data/lib/agents/{gpt_agents → agents}/information_retrieval_gpt_agent.rb +1 -1
  13. data/lib/agents/{gpt_agents → agents}/promptless_gpt_agent.rb +1 -1
  14. data/lib/agents/{gpt_agents → agents}/todo_gpt_agent.rb +1 -1
  15. data/lib/agents/{unhandleable_request_agent.rb → agents/unhandleable_request_agent.rb} +1 -1
  16. data/lib/agents/version.rb +3 -0
  17. data/lib/agents.rb +22 -28
  18. data/test/test_actions.rb +24 -69
  19. data/test/test_openai.rb +21 -41
  20. data/test/test_with_children.rb +75 -0
  21. metadata +31 -33
  22. data/lib/agents/agent.rb +0 -24
  23. data/lib/agents/dispatcher.rb +0 -29
  24. data/lib/agents/echo_agent.rb +0 -12
  25. data/lib/agents/gpt_agents/dispatching_gpt_agent.rb +0 -27
  26. data/lib/agents/gpt_agents/gpt_agent.rb +0 -29
  27. data/lib/version.rb +0 -3
  28. data/test/test_dispatcher.rb +0 -38
  29. data/test/test_gpt_dispatcher.rb +0 -25
  30. data/test/test_todo_gpt_agent.rb +0 -51
  31. /data/lib/{actions → agents/actions}/action_argument.rb +0 -0
  32. /data/lib/{actions → agents/actions}/action_example.rb +0 -0
  33. /data/lib/agents/{gpt_agents → agents}/echo_gpt_agent.rb +0 -0
  34. /data/lib/{gpt_clients → agents/gpt_clients}/echo_gpt_client.rb +0 -0
  35. /data/lib/{gpt_clients → agents/gpt_clients}/gpt_client.rb +0 -0
  36. /data/lib/{gpt_clients → agents/gpt_clients}/open_ai_gpt_client.rb +0 -0
  37. /data/lib/{logger.rb → agents/logger.rb} +0 -0
  38. /data/lib/{requests → agents/requests}/request.rb +0 -0
  39. /data/lib/{responses → agents/responses}/gpt_response.rb +0 -0
  40. /data/lib/{responses → agents/responses}/response.rb +0 -0
  41. /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: ca0396754afd0be4aac5df2f2c120e72d2898eea74f52df8fdfc8c414a941e70
4
- data.tar.gz: 8805783b3594ed85b56658d2a0f8e9386ce6e9cdf1f18c4a9eaa0d12c6f7a7f6
3
+ metadata.gz: dea36bf15988aaf769096d19041e9358317ff6a8dc277a418514e0d8e957f8f2
4
+ data.tar.gz: 183361122b78107dde2646db255871746c5b380ff2c7e483b762c22ccb17d21f
5
5
  SHA512:
6
- metadata.gz: 1b08f556416b78e11f9ab6d13f25c3f7f6b7a5c841c859b70d674fe37c91f62f36a3734ee0a388aea0584a5786e495e38b8ab3eb513265a95bfadc46cb1af8db
7
- data.tar.gz: 5dba6e632f9d6cf3c452a796297fbcb191a02296cb0197889bfdc69b28d581f454b74882f47b0a56126ca015b0ce5be2bbb7dd250fa3d316d3af6ca65cd24e47
6
+ metadata.gz: d8194908ff998e405a9893e6a663e5b58bd82849230ea753ba04da498eca905539f7db5dd91cf8167093aec59f503685e83d8bc262255c18be3c66af7dbcbc12
7
+ data.tar.gz: 97f6f490797b0e1c488ce7fda4688893eed2702dcc6ab41f66ea1cef942192764c6457e17d8cec8b82191fdcbf2698a27dfc4f9af41a46edd11596a5f2ec4d8f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-agents (0.1.2)
4
+ ruby-agents (0.2.2)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
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.
data/agents.gemspec CHANGED
@@ -1,25 +1,22 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'logger'
5
- require_relative 'lib/version'
4
+ require_relative 'lib/agents/version'
6
5
 
7
6
  Gem::Specification.new do |s|
8
- s.name = "ruby-agents"
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
- # @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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Agents
2
+ VERSION = "0.2.2"
3
+ 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/dispatcher'
15
- require_relative 'agents/echo_agent'
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/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'
21
+ require_relative 'agents/agents/unhandleable_request_agent'
24
22
 
25
- require_relative 'agents/unhandleable_request_agent'
23
+ require_relative 'agents/requests/request'
26
24
 
27
- require_relative 'requests/request'
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 'responses/response'
30
- require_relative 'responses/gpt_response'
31
- require_relative 'responses/unhandleable_request_response'
32
-
33
- require_relative 'actions/action'
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
- 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,14 +1,14 @@
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.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-05 00:00:00.000000000 Z
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: Put AI to Work.
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/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
68
- - lib/agents/unhandleable_request_agent.rb
69
- - lib/gpt_clients/echo_gpt_client.rb
70
- - lib/gpt_clients/gpt_client.rb
71
- - lib/gpt_clients/open_ai_gpt_client.rb
72
- - lib/logger.rb
73
- - lib/requests/request.rb
74
- - lib/responses/gpt_response.rb
75
- - lib/responses/response.rb
76
- - lib/responses/unhandleable_request_response.rb
77
- - lib/version.rb
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/test_todo_gpt_agent.rb
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: '0'
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.3.7
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
@@ -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
data/lib/version.rb DELETED
@@ -1,3 +0,0 @@
1
- module Agents
2
- VERSION = "0.1.2"
3
- 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
File without changes
File without changes
File without changes
File without changes
File without changes