ruby-agents 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +59 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +48 -0
  5. data/LICENSE +21 -0
  6. data/README.md +43 -0
  7. data/agents.gemspec +25 -0
  8. data/lib/actions/action.rb +43 -0
  9. data/lib/actions/action_argument.rb +14 -0
  10. data/lib/actions/action_example.rb +14 -0
  11. data/lib/agents/agent.rb +24 -0
  12. data/lib/agents/dispatcher.rb +29 -0
  13. data/lib/agents/echo_agent.rb +12 -0
  14. data/lib/agents/gpt_agents/calendar_gpt_agent.rb +17 -0
  15. data/lib/agents/gpt_agents/categorizing_gpt_agent.rb +19 -0
  16. data/lib/agents/gpt_agents/dispatching_gpt_agent.rb +27 -0
  17. data/lib/agents/gpt_agents/echo_gpt_agent.rb +12 -0
  18. data/lib/agents/gpt_agents/gpt_agent.rb +29 -0
  19. data/lib/agents/gpt_agents/information_retrieval_gpt_agent.rb +17 -0
  20. data/lib/agents/gpt_agents/promptless_gpt_agent.rb +10 -0
  21. data/lib/agents/gpt_agents/todo_gpt_agent.rb +31 -0
  22. data/lib/agents/unhandleable_request_agent.rb +8 -0
  23. data/lib/agents.rb +39 -0
  24. data/lib/gpt_clients/echo_gpt_client.rb +11 -0
  25. data/lib/gpt_clients/gpt_client.rb +14 -0
  26. data/lib/gpt_clients/open_ai_gpt_client.rb +40 -0
  27. data/lib/logger.rb +13 -0
  28. data/lib/requests/request.rb +8 -0
  29. data/lib/responses/gpt_response.rb +51 -0
  30. data/lib/responses/response.rb +8 -0
  31. data/lib/responses/unhandleable_request_response.rb +8 -0
  32. data/lib/version.rb +3 -0
  33. data/test/test_actions.rb +87 -0
  34. data/test/test_dispatcher.rb +38 -0
  35. data/test/test_gpt_dispatcher.rb +25 -0
  36. data/test/test_openai.rb +73 -0
  37. data/test/test_todo_gpt_agent.rb +51 -0
  38. metadata +105 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca0396754afd0be4aac5df2f2c120e72d2898eea74f52df8fdfc8c414a941e70
4
+ data.tar.gz: 8805783b3594ed85b56658d2a0f8e9386ce6e9cdf1f18c4a9eaa0d12c6f7a7f6
5
+ SHA512:
6
+ metadata.gz: 1b08f556416b78e11f9ab6d13f25c3f7f6b7a5c841c859b70d674fe37c91f62f36a3734ee0a388aea0584a5786e495e38b8ab3eb513265a95bfadc46cb1af8db
7
+ data.tar.gz: 5dba6e632f9d6cf3c452a796297fbcb191a02296cb0197889bfdc69b28d581f454b74882f47b0a56126ca015b0ce5be2bbb7dd250fa3d316d3af6ca65cd24e47
data/.gitignore ADDED
@@ -0,0 +1,59 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+
58
+ .idea
59
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ruby-agents (0.1.2)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ attr_extras (7.1.0)
10
+ base64 (0.1.1)
11
+ concurrent-ruby (1.2.2)
12
+ diff-lcs (1.5.0)
13
+ event_stream_parser (0.3.0)
14
+ faraday (2.7.11)
15
+ base64
16
+ faraday-net_http (>= 2.0, < 3.1)
17
+ ruby2_keywords (>= 0.0.4)
18
+ faraday-multipart (1.0.4)
19
+ multipart-post (~> 2)
20
+ faraday-net_http (3.0.2)
21
+ multipart-post (2.3.0)
22
+ optimist (3.1.0)
23
+ patience_diff (1.2.0)
24
+ optimist (~> 3.0)
25
+ ruby-openai (6.0.0)
26
+ event_stream_parser (>= 0.3.0, < 1.0.0)
27
+ faraday (>= 1)
28
+ faraday-multipart (>= 1)
29
+ ruby2_keywords (0.0.5)
30
+ super_diff (0.10.0)
31
+ attr_extras (>= 6.2.4)
32
+ diff-lcs
33
+ patience_diff
34
+ tldr (0.9.5)
35
+ concurrent-ruby (~> 1.2)
36
+ super_diff (~> 0.10)
37
+
38
+ PLATFORMS
39
+ arm64-darwin-21
40
+ arm64-darwin-22
41
+
42
+ DEPENDENCIES
43
+ ruby-agents!
44
+ ruby-openai (>= 5.1)
45
+ tldr (>= 0.9.5)
46
+
47
+ BUNDLED WITH
48
+ 2.4.19
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Jeff McFadden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # agents
2
+
3
+ A ruby library for building and managing AI agents within your application.
4
+
5
+ ## Getting Started
6
+
7
+ Gemfile:
8
+
9
+ ```ruby
10
+ gem 'agents', git: "jeffmcfadden/agents"
11
+ ```
12
+
13
+ `bundle install`
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require 'agents'
19
+ ```
20
+
21
+ ### Built-in Agents
22
+
23
+ ### Building Your Own Agents
24
+
25
+ ### Using a Dispatcher
26
+
27
+ ### Running Actions
28
+
29
+
30
+ ## Testing
31
+
32
+ Use `tldr` to run the tests.
33
+
34
+ ```shell
35
+ $ bundle install
36
+ $ tldr --watch
37
+ ```
38
+
39
+ By default, no network calls are made while testing. If you want to test the full path into OpenAI, you can run that with:
40
+
41
+ ```shell
42
+ $ OPENAI_ACCESS_TOKEN="sk-123YourToken" bundle exec ruby test/test_openai.rb
43
+ ```
data/agents.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'logger'
5
+ require_relative 'lib/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "ruby-agents"
9
+ s.version = Agents::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+
12
+ s.authors = ["Jeff McFadden"]
13
+ s.date = "2023-11-05"
14
+ s.description = "Put AI to Work."
15
+ s.email = "55709+jeffmcfadden@users.noreply.github.com"
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.homepage = "https://github.com/jeffmcfadden/agents"
19
+ s.require_paths = ["lib"]
20
+ s.rubygems_version = Agents::VERSION
21
+ s.summary = "Setup and organize requests between multiple AI agents."
22
+
23
+ s.add_development_dependency "tldr", ">= 0.9.5"
24
+ s.add_development_dependency "ruby-openai", ">= 5.1"
25
+ end
@@ -0,0 +1,43 @@
1
+ module Agents
2
+
3
+ # Takes a block and executes it when the action is called.
4
+ class Action
5
+ attr_reader :name, :description, :arguments, :examples
6
+
7
+ # Create a new action
8
+ # @param [String] name
9
+ # @param [String] description
10
+ # @param [Array<ActionArgument>] arguments
11
+ # @param [Array<ActionExample>] examples
12
+ # @param [Block] block
13
+ def initialize(name:, description: "", arguments: [], examples: [], &block)
14
+ @name = name
15
+ @description = description
16
+ @arguments = arguments
17
+ @examples = examples
18
+ @block = block
19
+ end
20
+
21
+ # Execute the block with the given arguments.
22
+ def call(args)
23
+ @block.call(args)
24
+ end
25
+
26
+ def for_prompt
27
+ s = "#{name}: #{description}\n"
28
+
29
+ unless arguments.empty?
30
+ s << " Arguments:\n"
31
+ s << arguments.collect(&:for_prompt).join("\n") << "\n"
32
+ end
33
+
34
+ unless examples.empty?
35
+ s << " For example:\n"
36
+ s << examples.collect(&:for_prompt).join("\n") << "\n"
37
+ end
38
+
39
+ s
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ module Agents
2
+ class ActionArgument
3
+ attr_reader :name, :type, :description
4
+ def initialize(name:, type:, description:)
5
+ @name = name
6
+ @type = type
7
+ @description = description
8
+ end
9
+
10
+ def for_prompt
11
+ " #{name}: #{description}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Agents
2
+ class ActionExample
3
+ attr_reader :input, :output
4
+ def initialize(input:, output:)
5
+ @input = input
6
+ @output = output
7
+ end
8
+
9
+ def for_prompt
10
+ " Input: #{input}\n Output: #{output}"
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,17 @@
1
+ module Agents
2
+ class CalendarGptAgent < GptAgent
3
+
4
+ attr_reader :system_prompt
5
+ def initialize(**args)
6
+ super(**args)
7
+ @description = "I am an AI Assistant that specializes in Calendar and Scheduling Management for Appointments, meetings, etc."
8
+ @system_prompt = <<~EOS
9
+ You are a helpful assistant.
10
+ EOS
11
+ end
12
+
13
+ def handle(request:)
14
+ gpt_client.chat system_prompt: system_prompt, prompt: request.request_text
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Agents
2
+ class CategorizingGptAgent < GptAgent
3
+
4
+ attr_reader :system_prompt
5
+ def initialize(**args)
6
+ super(**args)
7
+ @system_prompt = <<~EOS
8
+ You are a helpful assistant specializing in categorization. For each request to follow,
9
+ please respond with a JSON object with a single key, 'category'. Each request will include
10
+ the valid values for the category key.
11
+ EOS
12
+ end
13
+
14
+ # @return [GPTResponse] response
15
+ def handle(request:)
16
+ gpt_client.chat system_prompt: system_prompt, prompt: request.request_text
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ # An agent that echoes back the request text. Useful for testing.
5
+ class EchoGptAgent < Agent
6
+
7
+ # @param [Request] request
8
+ def handle(request:)
9
+ GptResponse.new(request.request_text)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,17 @@
1
+ module Agents
2
+ class InformationRetrievalGptAgent < GptAgent
3
+
4
+ attr_reader :system_prompt
5
+ def initialize(**args)
6
+ super(**args)
7
+ @description = "I am an AI Assistant that specializes in fetching information and answering general questions, etc."
8
+ @system_prompt = <<~EOS
9
+ You are a helpful assistant.
10
+ EOS
11
+ end
12
+
13
+ def handle(request:)
14
+ gpt_client.chat system_prompt: system_prompt, prompt: request.request_text
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ module Agents
2
+ # An agent that does not add a prompt to the request, but simply passes it through.
3
+ # Use if your test _is _the prompt, for example.
4
+ class PromptlessGptAgent < GptAgent
5
+ def handle(request:)
6
+ Response.new(response_text: gpt_client.chat(prompt: request.request_text))
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ module Agents
2
+ class TodoGptAgent < GptAgent
3
+
4
+ def initialize(**args)
5
+ super(**args)
6
+ @description = "I am an AI Assistant that specializes in Todo List and Reminders Management."
7
+ @system_prompt = <<~EOS
8
+ You are a helpful assistant specializing in Todo List and Reminders Management. Please do your best to complete
9
+ any requests you are given. For each request, please respond ONLY with a JSON object. The JSON object should have
10
+ two top-level keys: `response` and `actions`. The `response` key should be a short string that summarizes the actions
11
+ to be taken. The `actions` key should be an array of JSON objects, each of which has a `name` key and an `args` key.
12
+ If you aren't sure what actions should be taken, or you don't think there's anything relevant, then respond with
13
+ an empty array for the `actions` key.
14
+
15
+
16
+ EOS
17
+ end
18
+
19
+ def system_prompt
20
+ prompt = @system_prompt
21
+
22
+ unless self.actions.empty?
23
+ prompt << "The following actions are supported: \n"
24
+ prompt << self.actions.collect(&:for_prompt).join("\n\n")
25
+ end
26
+
27
+ prompt << "\n\n"
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ module Agents
2
+ class UnhandleableRequestAgent
3
+ def handle(request:)
4
+ return UnhandleableRequestResponse.new(request: request)
5
+ end
6
+ end
7
+
8
+ end
data/lib/agents.rb ADDED
@@ -0,0 +1,39 @@
1
+ require_relative 'logger'
2
+ require_relative 'version'
3
+
4
+ require 'openai' rescue nil # OpenAI is optional
5
+
6
+ require_relative 'gpt_clients/gpt_client'
7
+ require_relative 'gpt_clients/echo_gpt_client'
8
+
9
+ if defined?(::OpenAI::Client)
10
+ require_relative 'gpt_clients/open_ai_gpt_client'
11
+ end
12
+
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'
24
+
25
+ require_relative 'agents/unhandleable_request_agent'
26
+
27
+ require_relative 'requests/request'
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
@@ -0,0 +1,11 @@
1
+ module Agents
2
+ class EchoGptClient < GptClient
3
+
4
+ # Returns the input prompt as the output.
5
+ # @param [String] prompt
6
+ # @return [GPTResponse] response The response will be the same as the prompt.
7
+ def chat(prompt: "", **args)
8
+ GptResponse.new(prompt.to_s)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agents
4
+ class GptClient
5
+ def initialize()
6
+ end
7
+
8
+ # Chats via the GPT Client, returns a GPTResponse
9
+ # @return [GPTResponse] response
10
+ def chat(prompt: "", **args)
11
+ GptResponse.new("Error: No GPT Client Setup.")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ module Agents
2
+ class OpenAiGptClient < GptClient
3
+ attr_reader :open_ai_client
4
+ attr_reader :default_params
5
+
6
+ def initialize(open_ai_client:, default_params: {}, **args)
7
+ super(**args)
8
+ @open_ai_client = open_ai_client
9
+ @default_params = {
10
+ model: "gpt-3.5-turbo-1106",
11
+ temperature: 0.7,
12
+ }.merge(default_params)
13
+ end
14
+
15
+ def chat(prompt: "", **args)
16
+ messages = []
17
+ messages << { role: "system", content: "#{args[:system_prompt] || 'You are a helpful assistant.'}" }
18
+ messages << { role: "user", content: "#{prompt}" }
19
+
20
+ Agents.logger.debug("Sending to ChatGPT:")
21
+ Agents.logger.debug(messages)
22
+
23
+ parameters = default_params.merge({messages: messages})
24
+
25
+ completions = open_ai_client.chat parameters: parameters
26
+
27
+ Agents.logger.debug("ChatGPT Responded:")
28
+ Agents.logger.debug(completions)
29
+
30
+ text = completions.dig("choices", 0, "message", "content")
31
+
32
+ Agents.logger.debug("-" * 80)
33
+ Agents.logger.debug text
34
+ Agents.logger.debug("-" * 80)
35
+
36
+ GptResponse.new(text)
37
+ end
38
+
39
+ end
40
+ end
data/lib/logger.rb ADDED
@@ -0,0 +1,13 @@
1
+ #frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Agents
6
+ def self.logger=(logger)
7
+ @logger = logger
8
+ end
9
+
10
+ def self.logger
11
+ @logger ||= Logger.new(STDOUT)
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module Agents
2
+ class Request
3
+ attr_reader :request_text
4
+ def initialize(request_text)
5
+ @request_text = request_text
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,51 @@
1
+ module Agents
2
+ # The response from a GPT request.
3
+ class GptResponse < Response
4
+
5
+ attr_reader :raw_text
6
+ attr_reader :response_text
7
+ attr_reader :suggested_actions
8
+ attr_reader :data
9
+
10
+
11
+ # @param [String] raw_text The raw text the GPT service returned.
12
+ def initialize(raw_text)
13
+ @raw_text = raw_text
14
+
15
+ begin
16
+ @data = extract_json_data(raw_text)
17
+ @response_text = data["response"] || raw_text
18
+ @suggested_actions = data["actions"] || []
19
+ rescue
20
+ @data = {}
21
+ @response_text = raw_text
22
+ @suggested_actions = []
23
+ end
24
+ end
25
+
26
+ # TODO: Extract this into an object:
27
+ def extract_json_data(string)
28
+ # Check if the string is valid JSON.
29
+ begin
30
+ json_obj = JSON.parse(string)
31
+ return json_obj
32
+ rescue JSON::ParserError => error
33
+ Agents.logger.info "Whole string was not pure JSON. #{error}"
34
+ end
35
+
36
+ # Search for JSON using regular expression
37
+ match_data = string.match(/\{.*\}/m)
38
+ if match_data
39
+ begin
40
+ Agents.logger.info "Found some JSON in the string."
41
+
42
+ json_obj = JSON.parse(match_data[0])
43
+ return json_obj
44
+ rescue JSON::ParserError
45
+ Agents.logger.info "Extracted string was not pure JSON. #{error}"
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,8 @@
1
+ module Agents
2
+ class Response
3
+ attr_reader :response_text
4
+ def initialize(response_text)
5
+ @response_text = response_text
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Agents
2
+ class UnhandleableRequestResponse < Response
3
+ def initialize(request:)
4
+ super("Sorry, I'm not sure how to handle that.")
5
+ end
6
+ end
7
+
8
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Agents
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,87 @@
1
+ require 'agents'
2
+
3
+ module Agents
4
+ class TestActions < TLDR
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
13
+
14
+ def test_dispatching_prompt
15
+ test_action = Action.new(name: "test_action") do |args|
16
+ puts "This is a test action"
17
+ 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
58
+ end
59
+ end
60
+
61
+ class TestActionPerformingGptAgent < GptAgent
62
+
63
+ end
64
+
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)
75
+
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: {} }] }
78
+
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
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This actually tests the full Open AI call path. It can be slow, and tldr will
4
+ # fail after 1.8 seconds, so you need to run this separately.
5
+
6
+ # Run this file with:
7
+ # OPENAI_ACCESS_TOKEN="sk-123YourToken" bundle exec ruby test/test_openai.rb
8
+ require 'openai'
9
+ require 'agents'
10
+
11
+ module Agents
12
+
13
+ class ActionPerformingGptAgent < GptAgent
14
+ end
15
+
16
+ class TestOpenAi
17
+ def initialize
18
+ @openai_client = OpenAI::Client.new(access_token: ENV['OPENAI_ACCESS_TOKEN'])
19
+ @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
+
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))
37
+
38
+ response = dispatcher.handle(request: Request.new("I need to buy break and milk"))
39
+ puts response
40
+ end
41
+
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
+
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
63
+ end
64
+
65
+ end
66
+ end
67
+
68
+ # Skip this test if we don't have an access token
69
+ unless ENV['OPENAI_ACCESS_TOKEN'].nil?
70
+ # Agents::TestOpenAi.new.test_basic_chat
71
+ # Agents::TestOpenAi.new.test_dispatching_chat
72
+ Agents::TestOpenAi.new.test_todo_action
73
+ end
@@ -0,0 +1,51 @@
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
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-agents
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Jeff McFadden
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tldr
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.9.5
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.9.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-openai
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '5.1'
41
+ description: Put AI to Work.
42
+ email: 55709+jeffmcfadden@users.noreply.github.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - ".gitignore"
48
+ - Gemfile
49
+ - Gemfile.lock
50
+ - LICENSE
51
+ - README.md
52
+ - agents.gemspec
53
+ - lib/actions/action.rb
54
+ - lib/actions/action_argument.rb
55
+ - lib/actions/action_example.rb
56
+ - 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
78
+ - test/test_actions.rb
79
+ - test/test_dispatcher.rb
80
+ - test/test_gpt_dispatcher.rb
81
+ - test/test_openai.rb
82
+ - test/test_todo_gpt_agent.rb
83
+ homepage: https://github.com/jeffmcfadden/agents
84
+ licenses: []
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.3.7
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Setup and organize requests between multiple AI agents.
105
+ test_files: []