bristow 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +61 -58
- data/examples/agency.rb +1 -2
- data/examples/function_calls.rb +15 -4
- data/lib/bristow/agencies/supervisor.rb +0 -3
- data/lib/bristow/agency.rb +0 -6
- data/lib/bristow/agent.rb +1 -25
- data/lib/bristow/agents/supervisor.rb +11 -3
- data/lib/bristow/function.rb +10 -51
- data/lib/bristow/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a5ea707c57046701bd21f3ab1b286844084f4f537363a6bd8db314c9508dd4e
|
4
|
+
data.tar.gz: f58d69fd68907482a59aa5322be3981ce6ecf696f0721106bd416fded84a9628
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c1d8e1fff1d361cb58e1f35c05c81271c791d81c38315d6be8d79ccbbe2228fcf9e48ad8ccd6ffb119e6c92bcfb29587627e385d8a6ed7405f1b7e926afcd4e
|
7
|
+
data.tar.gz: 7ff6f846b72d9fd93930583386de052e0e1d27774ebc35e632ee69d53c306fead5841ec130abdbff2f3c8b6c315ee0092d8fba373dbe7856bb341c216d2d75d8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,6 @@
|
|
1
1
|
# Bristow
|
2
2
|
|
3
|
-
Bristow
|
4
|
-
|
5
|
-
## Features
|
6
|
-
|
7
|
-
- 🤖 Simple function-calling interface for GPT models
|
8
|
-
- 🔄 Automatic handling of OpenAI's function calling protocol
|
9
|
-
- 🛠 Type-safe function definitions
|
10
|
-
- 🔌 Easy to extend with custom functions
|
11
|
-
- 📝 Clean conversation management
|
12
|
-
- 🏢 Multi-agent coordination through agencies
|
3
|
+
Bristow makes working with AI models in your application dead simple. Whether it's a simple chat, using function calls, or building multi-agent systems, Bristow will help you hit the ground running.
|
13
4
|
|
14
5
|
## Installation
|
15
6
|
|
@@ -27,25 +18,6 @@ Or install it yourself as:
|
|
27
18
|
|
28
19
|
$ gem install bristow
|
29
20
|
|
30
|
-
## Configuration
|
31
|
-
|
32
|
-
Configure Bristow with your settings:
|
33
|
-
|
34
|
-
```ruby
|
35
|
-
Bristow.configure do |config|
|
36
|
-
# Your OpenAI API key (defaults to ENV['OPENAI_API_KEY'])
|
37
|
-
config.openai_api_key = 'your-api-key'
|
38
|
-
|
39
|
-
# The default model to use (defaults to 'gpt-4o-mini')
|
40
|
-
config.default_model = 'gpt-4o'
|
41
|
-
|
42
|
-
# Logger to use (defaults to Logger.new(STDOUT))
|
43
|
-
config.logger = Rails.logger
|
44
|
-
end
|
45
|
-
```
|
46
|
-
|
47
|
-
These settings can be overridden on a per-agent basis when needed.
|
48
|
-
|
49
21
|
## Usage
|
50
22
|
|
51
23
|
### Simple Agent
|
@@ -61,18 +33,14 @@ storyteller = Bristow::Agent.new(
|
|
61
33
|
system_message: 'Given a topic, you will tell a brief spy story',
|
62
34
|
)
|
63
35
|
|
64
|
-
#
|
65
|
-
|
66
|
-
print
|
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
|
67
39
|
end
|
68
40
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
'Make it about a double agent'
|
73
|
-
]) do |part|
|
74
|
-
print part
|
75
|
-
end
|
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']
|
76
44
|
```
|
77
45
|
|
78
46
|
### Basic Agent with Functions
|
@@ -82,16 +50,26 @@ end
|
|
82
50
|
weather = Bristow::Function.new(
|
83
51
|
name: "get_weather",
|
84
52
|
description: "Get the current weather for a location",
|
85
|
-
parameters: {
|
86
|
-
|
87
|
-
|
53
|
+
parameters: {
|
54
|
+
properties: {
|
55
|
+
location: {
|
56
|
+
type: "string",
|
57
|
+
description: "The city and state, e.g. San Francisco, CA"
|
58
|
+
},
|
59
|
+
unit: {
|
60
|
+
type: "string",
|
61
|
+
enum: ["celsius", "fahrenheit"],
|
62
|
+
description: "The unit of temperature to return"
|
63
|
+
}
|
64
|
+
},
|
65
|
+
required: ["location"]
|
88
66
|
}
|
89
67
|
) do |location:, unit: 'celsius'|
|
90
|
-
#
|
68
|
+
# Implement your application logic here
|
91
69
|
{ temperature: 22, unit: unit }
|
92
70
|
end
|
93
71
|
|
94
|
-
# Create an agent with
|
72
|
+
# Create an agent with access to the function
|
95
73
|
weather_agent = Bristow::Agent.new(
|
96
74
|
name: "WeatherAssistant",
|
97
75
|
description: "Helps with weather-related queries",
|
@@ -99,17 +77,20 @@ weather_agent = Bristow::Agent.new(
|
|
99
77
|
)
|
100
78
|
|
101
79
|
# Chat with the agent
|
102
|
-
weather_agent.chat("What's the weather like in London?") do |
|
103
|
-
print
|
80
|
+
weather_agent.chat("What's the weather like in London?") do |response_chunk|
|
81
|
+
print response_chunk
|
104
82
|
end
|
105
83
|
```
|
106
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
|
+
|
107
88
|
### Multi-Agent System
|
108
89
|
|
109
|
-
You can coordinate multiple agents using
|
90
|
+
You can coordinate multiple agents using `Bristow::Agency`. Bristow includes a few common patterns, including the one like [langchain's multi-agent supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/). Here's how to use the supervisor agency:
|
110
91
|
|
111
92
|
```ruby
|
112
|
-
# Create specialized agents
|
93
|
+
# Create specialized agents. These can be configured with functions, as well.
|
113
94
|
pirate_talker = Bristow::Agent.new(
|
114
95
|
name: "PirateSpeaker",
|
115
96
|
description: "Agent for translating input to pirate-speak",
|
@@ -125,21 +106,43 @@ travel_agent = Bristow::Agent.new(
|
|
125
106
|
# Create a supervisor agency to coordinate the agents
|
126
107
|
agency = Bristow::Agencies::Supervisor.create(agents: [pirate_talker, travel_agent])
|
127
108
|
|
128
|
-
# The supervisor will automatically delegate to the appropriate agent.
|
129
|
-
# In this case, it will almost certainly delegate to the travel_agent first, to get a bulleted itenerary.
|
130
|
-
# Then, it will delegate to the pirate_talker to translate the itenerary into pirate-speak.
|
109
|
+
# The supervisor will automatically delegate to the appropriate agent as needed before generating a response for the user.
|
131
110
|
agency.chat([
|
132
111
|
{ role: "user", content: "I want to go to New York. Tell me about it as if you were a pirate." }
|
133
|
-
]) do |
|
134
|
-
print
|
112
|
+
]) do |response_chunk|
|
113
|
+
print response_chunk
|
135
114
|
end
|
136
115
|
```
|
137
116
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
117
|
+
## Examples
|
118
|
+
|
119
|
+
A few working examples are in the `examples/` directory. If you have `OPENAI_API_KEY` set in the environment, you can run the examples with with `bundle exec ruby examples/<example file>.rb` to get a taste of Bristow.
|
120
|
+
|
121
|
+
## Configuration
|
122
|
+
|
123
|
+
Configure Bristow with your settings:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Bristow.configure do |config|
|
127
|
+
# Your OpenAI API key (defaults to ENV['OPENAI_API_KEY'])
|
128
|
+
config.openai_api_key = 'your-api-key'
|
129
|
+
|
130
|
+
# The default model to use (defaults to 'gpt-4o-mini')
|
131
|
+
config.default_model = 'gpt-4o'
|
132
|
+
|
133
|
+
# Logger to use (defaults to Logger.new(STDOUT))
|
134
|
+
config.logger = Rails.logger
|
135
|
+
end
|
136
|
+
|
137
|
+
# You can overrided these settings on a per-agent basis like this:
|
138
|
+
storyteller = Bristow::Agent.new(
|
139
|
+
name: 'Sydney',
|
140
|
+
description: 'Agent for telling spy stories',
|
141
|
+
system_message: 'Given a topic, you will tell a brief spy story',
|
142
|
+
model: 'gpt-4o-mini',
|
143
|
+
logger: Logger.new(STDOUT)
|
144
|
+
)
|
145
|
+
```
|
143
146
|
|
144
147
|
## Development
|
145
148
|
|
data/examples/agency.rb
CHANGED
data/examples/function_calls.rb
CHANGED
@@ -9,8 +9,19 @@ weather = Bristow::Function.new(
|
|
9
9
|
name: "get_weather",
|
10
10
|
description: "Get the current weather for a location",
|
11
11
|
parameters: {
|
12
|
-
|
13
|
-
|
12
|
+
type: "object",
|
13
|
+
properties: {
|
14
|
+
location: {
|
15
|
+
type: "string",
|
16
|
+
description: "The city and state, e.g. San Francisco, CA"
|
17
|
+
},
|
18
|
+
unit: {
|
19
|
+
type: "string",
|
20
|
+
enum: ["celsius", "fahrenheit"],
|
21
|
+
description: "The unit of temperature to return"
|
22
|
+
}
|
23
|
+
},
|
24
|
+
required: [:location]
|
14
25
|
}
|
15
26
|
) do |location:, unit: 'celsius'|
|
16
27
|
# Your weather API call here
|
@@ -33,7 +44,7 @@ messages_from_chat = weather_agent.chat(messages) do |part|
|
|
33
44
|
print part
|
34
45
|
end
|
35
46
|
|
36
|
-
|
47
|
+
puts ''
|
37
48
|
puts '*' * 10
|
38
49
|
puts 'All messages:'
|
39
|
-
pp
|
50
|
+
pp messages
|
@@ -25,9 +25,6 @@ module Bristow
|
|
25
25
|
# Convert string message to proper format
|
26
26
|
messages = [{ role: "user", content: messages }] if messages.is_a?(String)
|
27
27
|
|
28
|
-
# Convert array of strings to proper format
|
29
|
-
messages = messages.map { |msg| msg.is_a?(String) ? { role: "user", content: msg } : msg } if messages.is_a?(Array)
|
30
|
-
|
31
28
|
supervisor.chat(messages, &block)
|
32
29
|
end
|
33
30
|
end
|
data/lib/bristow/agency.rb
CHANGED
@@ -7,12 +7,6 @@ module Bristow
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def chat(messages, &block)
|
10
|
-
# Convert string message to proper format
|
11
|
-
messages = [{ role: "user", content: messages }] if messages.is_a?(String)
|
12
|
-
|
13
|
-
# Convert array of strings to proper format
|
14
|
-
messages = messages.map { |msg| msg.is_a?(String) ? { role: "user", content: msg } : msg } if messages.is_a?(Array)
|
15
|
-
|
16
10
|
raise NotImplementedError, "Agency#chat must be implemented by a subclass"
|
17
11
|
end
|
18
12
|
|
data/lib/bristow/agent.rb
CHANGED
@@ -25,37 +25,13 @@ module Bristow
|
|
25
25
|
|
26
26
|
def functions_for_openai
|
27
27
|
functions.map do |function|
|
28
|
-
|
29
|
-
name: function.name,
|
30
|
-
description: function.description,
|
31
|
-
parameters: {
|
32
|
-
type: "object",
|
33
|
-
properties: function.parameters.transform_values { |type| parameter_type_for(type) },
|
34
|
-
required: function.parameters.keys.map(&:to_s)
|
35
|
-
}
|
36
|
-
}
|
28
|
+
function.to_openai_schema
|
37
29
|
end
|
38
30
|
end
|
39
31
|
|
40
32
|
def chat(messages, &block)
|
41
33
|
# Convert string message to proper format
|
42
34
|
messages = [{ role: "user", content: messages }] if messages.is_a?(String)
|
43
|
-
|
44
|
-
# Convert array to array of hashes
|
45
|
-
messages = if messages.is_a?(Array)
|
46
|
-
messages.map do |msg|
|
47
|
-
case msg
|
48
|
-
when String
|
49
|
-
{ role: "user", content: msg }
|
50
|
-
when Hash
|
51
|
-
msg.transform_keys(&:to_sym)
|
52
|
-
else
|
53
|
-
raise ArgumentError, "Invalid message format: #{msg.inspect}"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
else
|
57
|
-
messages
|
58
|
-
end
|
59
35
|
|
60
36
|
messages = messages.dup
|
61
37
|
messages.unshift(system_message_hash) if system_message
|
@@ -46,8 +46,16 @@ module Bristow
|
|
46
46
|
name: "delegate_to",
|
47
47
|
description: "Delegate a task to a specialized agent",
|
48
48
|
parameters: {
|
49
|
-
|
50
|
-
|
49
|
+
properties: {
|
50
|
+
agent_name: {
|
51
|
+
type: "string",
|
52
|
+
description: "The name of the agent to delegate to"
|
53
|
+
},
|
54
|
+
message: {
|
55
|
+
type: "string",
|
56
|
+
description: "The instructions for the agent being delegated to"
|
57
|
+
}
|
58
|
+
}
|
51
59
|
}
|
52
60
|
) do |agent_name:, message:|
|
53
61
|
raise "No agency set for supervisor" unless agency
|
@@ -66,4 +74,4 @@ module Bristow
|
|
66
74
|
end
|
67
75
|
end
|
68
76
|
end
|
69
|
-
end
|
77
|
+
end
|
data/lib/bristow/function.rb
CHANGED
@@ -10,10 +10,14 @@ module Bristow
|
|
10
10
|
@description = description
|
11
11
|
@parameters = parameters
|
12
12
|
@block = block
|
13
|
+
|
14
|
+
if !parameters.has_key?(:type)
|
15
|
+
parameters[:type] = "object"
|
16
|
+
end
|
13
17
|
end
|
14
18
|
|
15
19
|
def call(**kwargs)
|
16
|
-
|
20
|
+
validate_required_parameters!(kwargs)
|
17
21
|
@block.call(**kwargs)
|
18
22
|
end
|
19
23
|
|
@@ -21,61 +25,16 @@ module Bristow
|
|
21
25
|
{
|
22
26
|
name: name,
|
23
27
|
description: description,
|
24
|
-
parameters:
|
25
|
-
type: "object",
|
26
|
-
properties: parameters_schema,
|
27
|
-
required: required_parameters
|
28
|
-
}
|
28
|
+
parameters: parameters
|
29
29
|
}
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
33
|
|
34
|
-
def
|
35
|
-
|
36
|
-
missing =
|
37
|
-
raise ArgumentError, "missing keyword:
|
38
|
-
|
39
|
-
# Check parameter types
|
40
|
-
kwargs.each do |key, value|
|
41
|
-
expected_type = parameters[key].is_a?(Hash) ? parameters[key][:type] : parameters[key]
|
42
|
-
next unless expected_type.is_a?(Class) # Skip complex type definitions
|
43
|
-
|
44
|
-
unless value.is_a?(expected_type)
|
45
|
-
raise TypeError, "#{key} must be a #{expected_type}"
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def parameters_schema
|
51
|
-
parameters.transform_values do |type|
|
52
|
-
if type.is_a?(Hash)
|
53
|
-
{ type: ruby_type_to_json_type(type[:type]) }
|
54
|
-
else
|
55
|
-
{ type: ruby_type_to_json_type(type) }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def required_parameters
|
61
|
-
parameters.reject { |_, type| type.is_a?(Hash) && type[:optional] }.keys
|
62
|
-
end
|
63
|
-
|
64
|
-
def ruby_type_to_json_type(type)
|
65
|
-
case type
|
66
|
-
when String, Class
|
67
|
-
case type.to_s
|
68
|
-
when "String" then "string"
|
69
|
-
when "Integer" then "integer"
|
70
|
-
when "Float" then "number"
|
71
|
-
when "TrueClass", "FalseClass", "Boolean" then "boolean"
|
72
|
-
when "Array" then "array"
|
73
|
-
when "Hash" then "object"
|
74
|
-
else "string" # Default to string for unknown types
|
75
|
-
end
|
76
|
-
else
|
77
|
-
"string" # Default to string for unknown types
|
78
|
-
end
|
34
|
+
def validate_required_parameters!(kwargs)
|
35
|
+
required_params = parameters.dig(:required) || []
|
36
|
+
missing = required_params.map(&:to_sym) - kwargs.keys.map(&:to_sym)
|
37
|
+
raise ArgumentError, "missing keyword: #{missing.first}" if missing.any?
|
79
38
|
end
|
80
39
|
end
|
81
40
|
end
|
data/lib/bristow/version.rb
CHANGED
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.1
|
4
|
+
version: 0.2.1
|
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-01-30 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: ruby-openai
|
@@ -23,6 +23,20 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 7.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: debug
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.10'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.10'
|
26
40
|
- !ruby/object:Gem::Dependency
|
27
41
|
name: rake
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|