bristow 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/CHANGELOG.md +15 -0
- data/README.md +65 -60
- data/examples/agency.rb +22 -18
- data/examples/basic_agent.rb +17 -10
- data/examples/function_calls.rb +19 -20
- data/lib/bristow/agencies/supervisor.rb +11 -15
- data/lib/bristow/agency.rb +19 -4
- data/lib/bristow/agent.rb +33 -31
- data/lib/bristow/agents/supervisor.rb +34 -46
- data/lib/bristow/configuration.rb +2 -5
- data/lib/bristow/function.rb +23 -20
- data/lib/bristow/functions/delegate.rb +71 -0
- data/lib/bristow/helpers/delegate.rb +17 -0
- data/lib/bristow/helpers/sgetter.rb +42 -0
- data/lib/bristow/version.rb +1 -1
- data/lib/bristow.rb +7 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41e9fcfe099defc5696c4ef49b8bce8b63091de906e7cd7558a9e89a98734360
|
4
|
+
data.tar.gz: c19e7c5f20f658d8bc9e669513a9df4f730bc8a6595009866d096011d85f0751
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa99a514a81713700df0e78f8dafedd26e49e511edb1e2fd4c83a56ad6310c082d05cf61f7183bd8c00f4844f94632251f1bb6db098e7b18a04e213e0c9454ac
|
7
|
+
data.tar.gz: 16ab56ba600e68525b5c12dd7186c9aa1b2d3be28913369a6b1eb50f8e14bf34daba9188dd9c77540de2eb0da1fa0a4bd48889c85f502a610d0b19883faa7581
|
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.0] - 2025-02-01
|
4
|
+
|
5
|
+
- Major update to the API. Now primarily class based.
|
6
|
+
Check the README for new details.
|
7
|
+
|
8
|
+
- Require updating CHANGELOG.md in each PR, but only in PRs.
|
9
|
+
|
10
|
+
## [0.2.1] - 2025-01-29
|
11
|
+
|
12
|
+
- Fixed schema for supervisor agent function call
|
13
|
+
|
14
|
+
## [0.2.0] - 2025-01-28
|
15
|
+
|
16
|
+
- Refactored the schema for function calls
|
17
|
+
|
3
18
|
## [0.1.0] - 2025-01-21
|
4
19
|
|
5
20
|
- Initial release
|
data/README.md
CHANGED
@@ -20,37 +20,20 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
### Simple Agent
|
24
|
-
|
25
|
-
You can create agents without functions for basic tasks:
|
26
|
-
|
27
23
|
```ruby
|
28
24
|
require 'bristow'
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
system_message: 'Given a topic, you will tell a brief spy story',
|
34
|
-
)
|
35
|
-
|
36
|
-
# Either stream the response with a block:
|
37
|
-
storyteller.chat('Tell me a story about Cold War era Berlin') do |response_chunk|
|
38
|
-
print response_chunk # response_chunk will be the next chunk of text in the response from the model
|
26
|
+
# Configure Bristow
|
27
|
+
Bristow.configure do |config|
|
28
|
+
config.api_key = ENV['OPENAI_API_KEY']
|
39
29
|
end
|
40
30
|
|
41
|
-
# Or work with the entire conversation once it's complete:
|
42
|
-
conversation = storyteller.chat('Tell me a story about Cold War era Berlin')
|
43
|
-
puts conversation.last['content']
|
44
|
-
```
|
45
|
-
|
46
|
-
### Basic Agent with Functions
|
47
|
-
|
48
|
-
```ruby
|
49
31
|
# Define functions that the model can call
|
50
|
-
|
51
|
-
name
|
52
|
-
description
|
53
|
-
|
32
|
+
class WeatherLookup < Bristow::Function
|
33
|
+
name "get_weather"
|
34
|
+
description "Get the current weather for a location"
|
35
|
+
system_message "You are a weather forecast assistant. Given a location, you will look up the weather forecast and provide a brief description."
|
36
|
+
parameters {
|
54
37
|
properties: {
|
55
38
|
location: {
|
56
39
|
type: "string",
|
@@ -64,52 +47,74 @@ weather = Bristow::Function.new(
|
|
64
47
|
},
|
65
48
|
required: ["location"]
|
66
49
|
}
|
67
|
-
|
68
|
-
|
69
|
-
|
50
|
+
|
51
|
+
def perform(location:, unit: 'celsius')
|
52
|
+
# Implement your application logic here
|
53
|
+
{ temperature: 22, unit: unit }
|
54
|
+
end
|
70
55
|
end
|
71
56
|
|
72
57
|
# Create an agent with access to the function
|
73
|
-
|
74
|
-
name
|
75
|
-
description
|
76
|
-
|
77
|
-
|
58
|
+
class Forecaster < Bristow::Agent
|
59
|
+
name "WeatherAssistant"
|
60
|
+
description "Helps with weather-related queries"
|
61
|
+
system_message "You are a weather forecast assistant. Given a location, you will look up the weather forecast and provide a brief description."
|
62
|
+
functions [WeatherLookup]
|
63
|
+
end
|
78
64
|
|
79
65
|
# Chat with the agent
|
80
|
-
|
66
|
+
forecaster_agent = Forecaster.new
|
67
|
+
forecaster_agent.chat("What's the weather like in London?") do |response_chunk|
|
68
|
+
# As the agent streams the response, print each chunk
|
81
69
|
print response_chunk
|
82
70
|
end
|
83
|
-
```
|
84
|
-
|
85
|
-
The `parameters` hash is passed directly through to OpenAI's API. You can see details about how to define parameters in [OpenAI's documentation for defining functions](https://platform.openai.com/docs/guides/function-calling#defining-functions).
|
86
|
-
|
87
71
|
|
88
|
-
|
89
|
-
|
90
|
-
|
72
|
+
# Create a more complex agent that can access multiple functions
|
73
|
+
class WeatherDatabase < Bristow::Function
|
74
|
+
name "store_weather"
|
75
|
+
description "Store weather data in the database"
|
76
|
+
parameters {
|
77
|
+
properties: {
|
78
|
+
location: {
|
79
|
+
type: "string",
|
80
|
+
description: "The city and state, e.g. San Francisco, CA"
|
81
|
+
},
|
82
|
+
temperature: {
|
83
|
+
type: "number",
|
84
|
+
description: "The temperature in the specified unit"
|
85
|
+
},
|
86
|
+
unit: {
|
87
|
+
type: "string",
|
88
|
+
enum: ["celsius", "fahrenheit"],
|
89
|
+
description: "The unit of temperature"
|
90
|
+
}
|
91
|
+
},
|
92
|
+
required: ["location", "temperature", "unit"]
|
93
|
+
}
|
91
94
|
|
92
|
-
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
system_message: 'Given a text, you will translate it to pirate-speak.',
|
98
|
-
)
|
95
|
+
def self.perform(location:, temperature:, unit:)
|
96
|
+
# Store the weather data in your database
|
97
|
+
{ status: "success", message: "Weather data stored for #{location}" }
|
98
|
+
end
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
-
name
|
102
|
-
description
|
103
|
-
system_message
|
104
|
-
|
101
|
+
class WeatherManager < Bristow::Agent
|
102
|
+
name "WeatherManager"
|
103
|
+
description "Manages weather data collection and storage"
|
104
|
+
system_message "You are a weather data management assistant. You can look up weather data and store it in the database."
|
105
|
+
functions [WeatherLookup, WeatherDatabase]
|
106
|
+
end
|
105
107
|
|
106
|
-
# Create
|
107
|
-
agency
|
108
|
+
# Create an agency to coordinate multiple agents. The supervisor agent is a
|
109
|
+
# pre-configured agency that includes a supervisor agent that knows how to
|
110
|
+
# delegate tasks to other agents
|
111
|
+
class WeatherAgency < Bristow::Agencies::Supervisor
|
112
|
+
agents [Forecaster, WeatherManager]
|
113
|
+
end
|
108
114
|
|
109
|
-
#
|
110
|
-
agency.
|
111
|
-
|
112
|
-
]) do |response_chunk|
|
115
|
+
# Use the agency to coordinate multiple agents
|
116
|
+
agency = WeatherAgency.new
|
117
|
+
agency.chat("Can you check the weather in London and store it in the database?") do |response_chunk|
|
113
118
|
print response_chunk
|
114
119
|
end
|
115
120
|
```
|
@@ -128,7 +133,7 @@ Bristow.configure do |config|
|
|
128
133
|
config.openai_api_key = 'your-api-key'
|
129
134
|
|
130
135
|
# The default model to use (defaults to 'gpt-4o-mini')
|
131
|
-
config.
|
136
|
+
config.model = 'gpt-4o'
|
132
137
|
|
133
138
|
# Logger to use (defaults to Logger.new(STDOUT))
|
134
139
|
config.logger = Rails.logger
|
data/examples/agency.rb
CHANGED
@@ -1,23 +1,27 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative '../lib/bristow'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
travel_agent = Bristow::Agent.new(
|
10
|
-
name: "TravelAgent",
|
11
|
-
description: "Agent for planning trips",
|
12
|
-
system_message: 'Given a destination, you will plan a trip. You will respond with an itinerary that includes dates, times, and locations only.',
|
13
|
-
)
|
3
|
+
class PirateTalker < Bristow::Agent
|
4
|
+
name "PirateSpeaker"
|
5
|
+
description "Agent for translating input to pirate-speak"
|
6
|
+
system_message 'Given a text, you will translate it to pirate-speak.'
|
7
|
+
end
|
14
8
|
|
15
|
-
|
9
|
+
class TravelAgent < Bristow::Agent
|
10
|
+
name "TravelAgent"
|
11
|
+
description "Agent for planning trips"
|
12
|
+
system_message 'Given a destination, you will plan a trip. You will respond with an itinerary that includes dates, times, and locations only.'
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
print part
|
15
|
+
class PirateTravelAgency < Bristow::Agencies::Supervisor
|
16
|
+
agents [PirateTalker, TravelAgent]
|
17
|
+
custom_instructions "Once you know the destination, you should call the TravelAgent to plan the itenerary, then translate the itenerary to pirate-speak using PirateSpeaker before returning it to the user."
|
21
18
|
end
|
22
19
|
|
23
|
-
|
20
|
+
begin
|
21
|
+
messages = PirateTravelAgency.chat("I want to go to New York") do |part|
|
22
|
+
print part
|
23
|
+
end
|
24
|
+
rescue SystemStackError => e
|
25
|
+
puts e.backtrace.join("\n")
|
26
|
+
end
|
27
|
+
pp messages
|
data/examples/basic_agent.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative '../lib/bristow'
|
2
2
|
|
3
3
|
Bristow.configure do |config|
|
4
|
-
config.
|
4
|
+
config.model = 'gpt-4o-mini'
|
5
5
|
end
|
6
6
|
|
7
|
-
|
8
|
-
name
|
9
|
-
description
|
10
|
-
system_message
|
11
|
-
|
7
|
+
class Sydney < Bristow::Agent
|
8
|
+
name 'Sydney'
|
9
|
+
description 'Agent for telling spy stories'
|
10
|
+
system_message 'Given a topic, you will tell a brief spy story'
|
11
|
+
end
|
12
|
+
|
13
|
+
sydney = Sydney.new
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
begin
|
16
|
+
sydney.chat([{role: 'user', content: 'Cold war era Berlin'}]) do |part|
|
17
|
+
print part
|
18
|
+
end
|
19
|
+
|
20
|
+
rescue SystemStackError => e
|
21
|
+
puts e.backtrace.join("\n")
|
22
|
+
end
|
data/examples/function_calls.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative '../lib/bristow'
|
2
2
|
|
3
3
|
Bristow.configure do |config|
|
4
|
-
config.
|
4
|
+
config.model = 'gpt-4o-mini'
|
5
5
|
end
|
6
6
|
|
7
7
|
# Define functions that GPT can call
|
8
|
-
|
9
|
-
name
|
10
|
-
description
|
11
|
-
parameters
|
8
|
+
class WeatherLookup < Bristow::Function
|
9
|
+
name "get_weather"
|
10
|
+
description "Get the current weather for a location"
|
11
|
+
parameters ({
|
12
12
|
type: "object",
|
13
13
|
properties: {
|
14
14
|
location: {
|
@@ -22,25 +22,24 @@ weather = Bristow::Function.new(
|
|
22
22
|
}
|
23
23
|
},
|
24
24
|
required: [:location]
|
25
|
-
}
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
})
|
26
|
+
|
27
|
+
def perform(location:, unit: 'celsius')
|
28
|
+
# Your weather API call here
|
29
|
+
{ temperature: 22, unit: unit }
|
30
|
+
end
|
29
31
|
end
|
30
32
|
|
31
33
|
# Create an agent with these functions
|
32
|
-
|
33
|
-
name
|
34
|
-
description
|
35
|
-
|
36
|
-
|
34
|
+
class WeatherAgent < Bristow::Agent
|
35
|
+
name "WeatherAssistant"
|
36
|
+
description "Helps with weather-related queries"
|
37
|
+
system_message "You are a helpful weather assistant. You'll be asked about the weather, and should use the get_weather function to respond."
|
38
|
+
functions [WeatherLookup]
|
39
|
+
end
|
37
40
|
|
38
41
|
# Start a conversation
|
39
|
-
messages =
|
40
|
-
{ "role" => "user", "content" => "What's the weather like in London?" }
|
41
|
-
]
|
42
|
-
|
43
|
-
messages_from_chat = weather_agent.chat(messages) do |part|
|
42
|
+
messages = WeatherAgent.chat("What's the weather like in London?") do |part|
|
44
43
|
print part
|
45
44
|
end
|
46
45
|
|
@@ -1,30 +1,26 @@
|
|
1
1
|
module Bristow
|
2
2
|
module Agencies
|
3
3
|
class Supervisor < Agency
|
4
|
-
|
4
|
+
attr_reader :supervisor
|
5
5
|
|
6
|
-
|
7
|
-
agency = new(agents: agents)
|
8
|
-
supervisor = Agents::Supervisor.new(
|
9
|
-
available_agents: agents,
|
10
|
-
agency: agency
|
11
|
-
)
|
12
|
-
agency.supervisor = supervisor
|
13
|
-
agency.agents << supervisor
|
14
|
-
agency
|
15
|
-
end
|
6
|
+
sgetter :custom_instructions, default: nil
|
16
7
|
|
17
|
-
def initialize(agents:
|
8
|
+
def initialize(agents: self.class.agents.dup)
|
9
|
+
@custom_instructions = self.class.custom_instructions
|
18
10
|
@agents = agents
|
19
|
-
@supervisor =
|
11
|
+
@supervisor = Agents::Supervisor.new(
|
12
|
+
child_agents: agents,
|
13
|
+
agency: self,
|
14
|
+
custom_instructions: custom_instructions
|
15
|
+
)
|
20
16
|
end
|
21
17
|
|
22
18
|
def chat(messages, &block)
|
23
|
-
raise "
|
19
|
+
raise "Supervisor not set" unless supervisor
|
24
20
|
|
25
21
|
# Convert string message to proper format
|
26
22
|
messages = [{ role: "user", content: messages }] if messages.is_a?(String)
|
27
|
-
|
23
|
+
|
28
24
|
supervisor.chat(messages, &block)
|
29
25
|
end
|
30
26
|
end
|
data/lib/bristow/agency.rb
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
module Bristow
|
2
2
|
class Agency
|
3
|
-
|
3
|
+
include Bristow::Sgetter
|
4
4
|
|
5
|
-
|
5
|
+
sgetter :agents, default: []
|
6
|
+
|
7
|
+
def initialize(agents: self.class.agents.dup)
|
6
8
|
@agents = agents
|
7
9
|
end
|
8
10
|
|
11
|
+
def self.chat(...)
|
12
|
+
new.chat(...)
|
13
|
+
end
|
14
|
+
|
9
15
|
def chat(messages, &block)
|
10
|
-
raise NotImplementedError, "
|
16
|
+
raise NotImplementedError, "#{self.class.name}#chat must be implemented"
|
11
17
|
end
|
12
18
|
|
13
19
|
def find_agent(name)
|
14
|
-
agents.find { |agent| agent
|
20
|
+
agent = agents.find { |agent| agent_name(agent) == name }
|
21
|
+
return nil unless agent
|
22
|
+
|
23
|
+
agent.is_a?(Class) ? agent.new : agent
|
15
24
|
end
|
16
25
|
|
17
26
|
protected
|
@@ -20,5 +29,11 @@ module Bristow
|
|
20
29
|
response = agent.chat(messages, &block)
|
21
30
|
response.last # Return just the last message
|
22
31
|
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def agent_name(agent)
|
36
|
+
agent.is_a?(Class) ? agent.name : agent.class.name
|
37
|
+
end
|
23
38
|
end
|
24
39
|
end
|
data/lib/bristow/agent.rb
CHANGED
@@ -1,19 +1,32 @@
|
|
1
|
-
require 'openai'
|
2
|
-
require 'logger'
|
3
|
-
require 'json'
|
4
|
-
|
5
1
|
module Bristow
|
6
2
|
class Agent
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
include Bristow::Sgetter
|
4
|
+
|
5
|
+
sgetter :name
|
6
|
+
sgetter :description
|
7
|
+
sgetter :system_message
|
8
|
+
sgetter :functions, default: []
|
9
|
+
sgetter :model, default: -> { Bristow.configuration.model }
|
10
|
+
sgetter :client, default: -> { Bristow.configuration.client }
|
11
|
+
sgetter :logger, default: -> { Bristow.configuration.logger }
|
12
|
+
attr_reader :chat_history
|
13
|
+
|
14
|
+
def initialize(
|
15
|
+
name: self.class.name,
|
16
|
+
description: self.class.name,
|
17
|
+
system_message: self.class.system_message,
|
18
|
+
functions: self.class.functions.dup,
|
19
|
+
model: self.class.model,
|
20
|
+
client: self.class.client,
|
21
|
+
logger: self.class.logger
|
22
|
+
)
|
10
23
|
@name = name
|
11
24
|
@description = description
|
12
25
|
@system_message = system_message
|
13
26
|
@functions = functions
|
14
|
-
@logger = Bristow.configuration.logger
|
15
|
-
@client = Bristow.configuration.client
|
16
27
|
@model = model
|
28
|
+
@client = client
|
29
|
+
@logger = logger
|
17
30
|
@chat_history = []
|
18
31
|
end
|
19
32
|
|
@@ -24,9 +37,11 @@ module Bristow
|
|
24
37
|
end
|
25
38
|
|
26
39
|
def functions_for_openai
|
27
|
-
functions.map
|
28
|
-
|
29
|
-
|
40
|
+
functions.map(&:to_openai_schema)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.chat(...)
|
44
|
+
new.chat(...)
|
30
45
|
end
|
31
46
|
|
32
47
|
def chat(messages, &block)
|
@@ -40,7 +55,7 @@ module Bristow
|
|
40
55
|
|
41
56
|
loop do
|
42
57
|
params = {
|
43
|
-
model:
|
58
|
+
model: model,
|
44
59
|
messages: messages
|
45
60
|
}
|
46
61
|
|
@@ -49,10 +64,11 @@ module Bristow
|
|
49
64
|
params[:function_call] = "auto"
|
50
65
|
end
|
51
66
|
|
67
|
+
logger.debug("Calling OpenAI API with params: #{params}")
|
52
68
|
response_message = if block_given?
|
53
69
|
handle_streaming_chat(params, &block)
|
54
70
|
else
|
55
|
-
response =
|
71
|
+
response = client.chat(parameters: params)
|
56
72
|
response.dig("choices", 0, "message")
|
57
73
|
end
|
58
74
|
|
@@ -80,7 +96,7 @@ module Bristow
|
|
80
96
|
|
81
97
|
messages
|
82
98
|
rescue Faraday::BadRequestError, Faraday::ResourceNotFound => e
|
83
|
-
|
99
|
+
logger.error("Error calling OpenAI API: #{e.response[:body]}")
|
84
100
|
raise
|
85
101
|
end
|
86
102
|
|
@@ -100,6 +116,7 @@ module Bristow
|
|
100
116
|
if delta.dig("function_call", "name")
|
101
117
|
function_name = delta.dig("function_call", "name")
|
102
118
|
end
|
119
|
+
|
103
120
|
if delta.dig("function_call", "arguments")
|
104
121
|
function_args += delta.dig("function_call", "arguments")
|
105
122
|
end
|
@@ -111,7 +128,7 @@ module Bristow
|
|
111
128
|
end
|
112
129
|
|
113
130
|
params[:stream] = stream_proc
|
114
|
-
|
131
|
+
client.chat(parameters: params)
|
115
132
|
|
116
133
|
if function_name
|
117
134
|
{
|
@@ -135,20 +152,5 @@ module Bristow
|
|
135
152
|
"content" => system_message
|
136
153
|
}
|
137
154
|
end
|
138
|
-
|
139
|
-
def parameter_type_for(type)
|
140
|
-
case type.to_s
|
141
|
-
when "Integer", "Fixnum"
|
142
|
-
{ type: "integer" }
|
143
|
-
when "Float"
|
144
|
-
{ type: "number" }
|
145
|
-
when "String"
|
146
|
-
{ type: "string" }
|
147
|
-
when "TrueClass", "FalseClass", "Boolean"
|
148
|
-
{ type: "boolean" }
|
149
|
-
else
|
150
|
-
{ type: "string" }
|
151
|
-
end
|
152
|
-
end
|
153
155
|
end
|
154
156
|
end
|
@@ -1,17 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bristow
|
2
4
|
module Agents
|
3
5
|
class Supervisor < Agent
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
attr_reader :agency
|
7
|
+
|
8
|
+
name "Supervisor"
|
9
|
+
description "A supervisor agent that coordinates between specialized agents"
|
10
|
+
system_message <<~MESSAGE
|
11
|
+
You are a supervisor agent that coordinates between specialized agents.
|
12
|
+
Your role is to:
|
13
|
+
1. Understand the user's request
|
14
|
+
2. Choose the most appropriate agent to handle it
|
15
|
+
3. Delegate using the delegate_to function
|
16
|
+
4. Review the agent's response
|
17
|
+
5. Either return the response to the user or delegate to another agent
|
18
|
+
|
19
|
+
After receiving a response, you can either:
|
20
|
+
1. Return it to the user if it fully answers their request
|
21
|
+
2. Delegate to another agent if more work is needed
|
22
|
+
3. Add your own clarification or summary if needed
|
23
|
+
MESSAGE
|
24
|
+
|
25
|
+
sgetter :custom_instructions, default: nil
|
26
|
+
|
27
|
+
def initialize(child_agents:, agency:, custom_instructions: nil)
|
28
|
+
super()
|
29
|
+
@custom_instructions = custom_instructions || self.class.custom_instructions
|
30
|
+
@system_message = build_system_message(child_agents)
|
14
31
|
@agency = agency
|
32
|
+
@custom_instructions = custom_instructions || self.class.custom_instructions
|
33
|
+
agency.agents << self
|
34
|
+
functions << Functions::Delegate.new(self, agency)
|
15
35
|
end
|
16
36
|
|
17
37
|
private
|
@@ -22,48 +42,16 @@ module Bristow
|
|
22
42
|
end.join("\n")
|
23
43
|
|
24
44
|
<<~MESSAGE
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
2. Choose the most appropriate agent to handle it
|
29
|
-
3. Delegate using the delegate_to function
|
30
|
-
4. Review the agent's response
|
31
|
-
5. Either return the response to the user or delegate to another agent
|
45
|
+
#{self.class.system_message}
|
46
|
+
|
47
|
+
#{custom_instructions.nil? ? "" : custom_instructions}
|
32
48
|
|
33
49
|
Available agents:
|
34
50
|
#{agent_descriptions}
|
35
51
|
|
36
52
|
Always use the delegate_to function to work with other agents.
|
37
|
-
After receiving a response, you can either:
|
38
|
-
1. Return it to the user if it fully answers their request
|
39
|
-
2. Delegate to another agent if more work is needed
|
40
|
-
3. Add your own clarification or summary if needed
|
41
53
|
MESSAGE
|
42
54
|
end
|
43
|
-
|
44
|
-
def delegate_function
|
45
|
-
Function.new(
|
46
|
-
name: "delegate_to",
|
47
|
-
description: "Delegate a task to a specialized agent",
|
48
|
-
parameters: {
|
49
|
-
agent_name: String,
|
50
|
-
message: String
|
51
|
-
}
|
52
|
-
) do |agent_name:, message:|
|
53
|
-
raise "No agency set for supervisor" unless agency
|
54
|
-
|
55
|
-
if agent_name == name
|
56
|
-
{ error: "Cannot delegate to self" }
|
57
|
-
else
|
58
|
-
agent = agency.find_agent(agent_name)
|
59
|
-
raise ArgumentError, "Agent #{agent_name} not found" unless agent
|
60
|
-
|
61
|
-
Bristow.configuration.logger.info("Delegating to #{agent_name}: #{message}")
|
62
|
-
response = agent.chat([{ "role" => "user", "content" => message }])
|
63
|
-
{ response: response.last["content"] }
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
55
|
end
|
68
56
|
end
|
69
|
-
end
|
57
|
+
end
|
@@ -1,14 +1,11 @@
|
|
1
|
-
require 'logger'
|
2
|
-
require 'openai'
|
3
|
-
|
4
1
|
module Bristow
|
5
2
|
class Configuration
|
6
|
-
attr_accessor :openai_api_key, :
|
3
|
+
attr_accessor :openai_api_key, :model, :logger
|
7
4
|
attr_reader :client
|
8
5
|
|
9
6
|
def initialize
|
10
7
|
@openai_api_key = ENV['OPENAI_API_KEY']
|
11
|
-
@
|
8
|
+
@model = 'gpt-4o-mini'
|
12
9
|
@logger = Logger.new(STDOUT)
|
13
10
|
reset_client
|
14
11
|
end
|
data/lib/bristow/function.rb
CHANGED
@@ -2,39 +2,42 @@
|
|
2
2
|
|
3
3
|
module Bristow
|
4
4
|
class Function
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(name:, description:, parameters:, &block)
|
8
|
-
raise ArgumentError, "block is required" unless block_given?
|
9
|
-
@name = name
|
10
|
-
@description = description
|
11
|
-
@parameters = parameters
|
12
|
-
@block = block
|
13
|
-
|
14
|
-
if !parameters.has_key?(:type)
|
15
|
-
parameters[:type] = "object"
|
16
|
-
end
|
17
|
-
end
|
5
|
+
include Bristow::Sgetter
|
6
|
+
include Bristow::Delegate
|
18
7
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
8
|
+
sgetter :name
|
9
|
+
sgetter :description
|
10
|
+
sgetter :parameters, default: {}
|
23
11
|
|
24
|
-
def to_openai_schema
|
12
|
+
def self.to_openai_schema
|
25
13
|
{
|
26
14
|
name: name,
|
27
15
|
description: description,
|
28
16
|
parameters: parameters
|
29
17
|
}
|
30
18
|
end
|
19
|
+
delegate :to_openai_schema, to: :class
|
20
|
+
|
21
|
+
def self.call(...)
|
22
|
+
new.call(...)
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(**kwargs)
|
26
|
+
validation_errors = validate_required_parameters!(kwargs)
|
27
|
+
return validation_errors unless validation_errors.nil?
|
28
|
+
perform(**kwargs)
|
29
|
+
end
|
30
|
+
|
31
|
+
def perform(...)
|
32
|
+
raise NotImplementedError, "#{self.class.name}#perform must be implemented"
|
33
|
+
end
|
31
34
|
|
32
35
|
private
|
33
36
|
|
34
37
|
def validate_required_parameters!(kwargs)
|
35
|
-
required_params = parameters.dig(:required) || []
|
38
|
+
required_params = self.class.parameters.dig(:required) || []
|
36
39
|
missing = required_params.map(&:to_sym) - kwargs.keys.map(&:to_sym)
|
37
|
-
|
40
|
+
"missing parameters: #{missing.inspect}" if missing.any?
|
38
41
|
end
|
39
42
|
end
|
40
43
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bristow
|
4
|
+
module Functions
|
5
|
+
class Delegate < Function
|
6
|
+
def self.name
|
7
|
+
"delegate_to"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.description
|
11
|
+
"Delegate a task to a specialized agent"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.parameters
|
15
|
+
{
|
16
|
+
type: "object",
|
17
|
+
properties: {
|
18
|
+
agent_name: {
|
19
|
+
type: "string",
|
20
|
+
description: "The name of the agent to delegate to"
|
21
|
+
},
|
22
|
+
message: {
|
23
|
+
type: "string",
|
24
|
+
description: "The instructions for the agent being delegated to"
|
25
|
+
}
|
26
|
+
},
|
27
|
+
required: ["agent_name", "message"]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
self.class.name
|
33
|
+
end
|
34
|
+
|
35
|
+
def description
|
36
|
+
self.class.description
|
37
|
+
end
|
38
|
+
|
39
|
+
def parameters
|
40
|
+
self.class.parameters
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(agent, agency)
|
44
|
+
raise ArgumentError, "Agent must not be nil" if agent.nil?
|
45
|
+
|
46
|
+
@agent = agent
|
47
|
+
@agency = agency
|
48
|
+
end
|
49
|
+
|
50
|
+
def agency=(agency)
|
51
|
+
@agency = agency
|
52
|
+
end
|
53
|
+
|
54
|
+
def perform(agent_name:, message:)
|
55
|
+
raise "Agency not set" if @agency.nil?
|
56
|
+
|
57
|
+
if agent_name == @agent.name
|
58
|
+
{ error: "Cannot delegate to self" }
|
59
|
+
else
|
60
|
+
agent = @agency.find_agent(agent_name)
|
61
|
+
raise ArgumentError, "Agent #{agent_name} not found" unless agent
|
62
|
+
|
63
|
+
Bristow.configuration.logger.info("Delegating to #{agent_name}: #{message}")
|
64
|
+
response = agent.chat([{ "role" => "user", "content" => message }])
|
65
|
+
last_message = response&.last
|
66
|
+
{ response: last_message&.[]("content") }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bristow
|
2
|
+
module Delegate
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def delegate(*methods, to:)
|
9
|
+
methods.each do |method|
|
10
|
+
define_method(method) do
|
11
|
+
send(to).send(method)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# A helper to define a getter/setter method
|
2
|
+
# This will let you define a class level getter/setter method with an instance reader
|
3
|
+
#
|
4
|
+
# Example:
|
5
|
+
# class Agent
|
6
|
+
# include Bristow::Sgetter
|
7
|
+
# sgetter :name
|
8
|
+
# sgetter :model, default: -> { Bristow.configuration.model }
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class Sydney < Agent
|
12
|
+
# name 'Sydney'
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# sydney = Sydney.new
|
16
|
+
# sydney.name # => 'Sydney'
|
17
|
+
module Bristow
|
18
|
+
module Sgetter
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def sgetter(attr, default: nil)
|
25
|
+
# Define the method that allows for both getter/setter syntax
|
26
|
+
define_singleton_method(attr) do |val = nil|
|
27
|
+
if val.nil?
|
28
|
+
return instance_variable_get("@#{attr}") if instance_variable_defined?("@#{attr}")
|
29
|
+
default.respond_to?(:call) ? default.call : default
|
30
|
+
else
|
31
|
+
instance_variable_set("@#{attr}", val)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Instance-level reader
|
36
|
+
define_method(attr) do
|
37
|
+
instance_variable_get("@#{attr}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/bristow/version.rb
CHANGED
data/lib/bristow.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'debug'
|
3
|
+
require 'openai'
|
4
|
+
require 'logger'
|
5
|
+
require 'json'
|
2
6
|
|
3
7
|
require_relative "bristow/version"
|
8
|
+
require_relative "bristow/helpers/sgetter"
|
9
|
+
require_relative "bristow/helpers/delegate"
|
4
10
|
require_relative "bristow/configuration"
|
5
11
|
require_relative "bristow/function"
|
12
|
+
require_relative "bristow/functions/delegate"
|
6
13
|
require_relative "bristow/agent"
|
7
14
|
require_relative "bristow/agents/supervisor"
|
8
15
|
require_relative "bristow/agency"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bristow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Hampton
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-01
|
10
|
+
date: 2025-02-01 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: ruby-openai
|
@@ -133,6 +133,9 @@ files:
|
|
133
133
|
- lib/bristow/agents/supervisor.rb
|
134
134
|
- lib/bristow/configuration.rb
|
135
135
|
- lib/bristow/function.rb
|
136
|
+
- lib/bristow/functions/delegate.rb
|
137
|
+
- lib/bristow/helpers/delegate.rb
|
138
|
+
- lib/bristow/helpers/sgetter.rb
|
136
139
|
- lib/bristow/version.rb
|
137
140
|
homepage: https://github.com/andrewhampton/bristow
|
138
141
|
licenses:
|
@@ -158,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
161
|
- !ruby/object:Gem::Version
|
159
162
|
version: '0'
|
160
163
|
requirements: []
|
161
|
-
rubygems_version: 3.6.
|
164
|
+
rubygems_version: 3.6.3
|
162
165
|
specification_version: 4
|
163
166
|
summary: A Ruby framework for creating systems of cooperative AI agents with function
|
164
167
|
calling capabilities
|