ruby-agents 0.1.2

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.
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: []