ruby-agents 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +59 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +21 -0
- data/README.md +43 -0
- data/agents.gemspec +25 -0
- data/lib/actions/action.rb +43 -0
- data/lib/actions/action_argument.rb +14 -0
- data/lib/actions/action_example.rb +14 -0
- data/lib/agents/agent.rb +24 -0
- data/lib/agents/dispatcher.rb +29 -0
- data/lib/agents/echo_agent.rb +12 -0
- data/lib/agents/gpt_agents/calendar_gpt_agent.rb +17 -0
- data/lib/agents/gpt_agents/categorizing_gpt_agent.rb +19 -0
- data/lib/agents/gpt_agents/dispatching_gpt_agent.rb +27 -0
- data/lib/agents/gpt_agents/echo_gpt_agent.rb +12 -0
- data/lib/agents/gpt_agents/gpt_agent.rb +29 -0
- data/lib/agents/gpt_agents/information_retrieval_gpt_agent.rb +17 -0
- data/lib/agents/gpt_agents/promptless_gpt_agent.rb +10 -0
- data/lib/agents/gpt_agents/todo_gpt_agent.rb +31 -0
- data/lib/agents/unhandleable_request_agent.rb +8 -0
- data/lib/agents.rb +39 -0
- data/lib/gpt_clients/echo_gpt_client.rb +11 -0
- data/lib/gpt_clients/gpt_client.rb +14 -0
- data/lib/gpt_clients/open_ai_gpt_client.rb +40 -0
- data/lib/logger.rb +13 -0
- data/lib/requests/request.rb +8 -0
- data/lib/responses/gpt_response.rb +51 -0
- data/lib/responses/response.rb +8 -0
- data/lib/responses/unhandleable_request_response.rb +8 -0
- data/lib/version.rb +3 -0
- data/test/test_actions.rb +87 -0
- data/test/test_dispatcher.rb +38 -0
- data/test/test_gpt_dispatcher.rb +25 -0
- data/test/test_openai.rb +73 -0
- data/test/test_todo_gpt_agent.rb +51 -0
- 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
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
|
data/lib/agents/agent.rb
ADDED
@@ -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,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
|
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,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
|
data/lib/version.rb
ADDED
@@ -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
|
data/test/test_openai.rb
ADDED
@@ -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: []
|