bristow 0.1.0 → 0.2.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/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/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: e61544a71407814e6c09e22d56631df8ed7a0475cf8b182ba4d7bf86bcf68d4c
|
4
|
+
data.tar.gz: 29a5285d24236fbd6226bffebbdd34becd3a7c2ac81c33fafde0a35d0e8eaf31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b9a904a1309e6d111ecf4b5172a4f07b6c57acfb7a8b219c4b890a6e914fcdc7e6bc9b7da5aa88b67aca1bffe57de185be2fe2583ab8fea8dedf416f67c1100
|
7
|
+
data.tar.gz: defc7c18b96063f1e1a4e88513ba7cc780c0fbf66aecf6f5eda74084721e4325a29a3f0ea3a50fec453309833280daa2db39aa3d782c548b86ee737dac1b92fe
|
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
|
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.
|
4
|
+
version: 0.2.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-01-29 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
|