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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa9d6f366bc13e51f9b2aac247ac4103c61afab9da22670be1ff60ab3a0c5031
4
- data.tar.gz: cb45d972b3597f4a525cfc91004702d8fd53ff19add9c414394be229edee159c
3
+ metadata.gz: e61544a71407814e6c09e22d56631df8ed7a0475cf8b182ba4d7bf86bcf68d4c
4
+ data.tar.gz: 29a5285d24236fbd6226bffebbdd34becd3a7c2ac81c33fafde0a35d0e8eaf31
5
5
  SHA512:
6
- metadata.gz: c4af5ed27634ea26ee20045434a515ef43c4ddc2ed6640a37601b48fac83c5cab3245fe046978a6fb9be03dc793f823d6bb709136f7e9496b63d223b1b042c0d
7
- data.tar.gz: cbd4c61ae965a4f238d8c0598c364235d3ff5dbfbf35c461e738c820ae49dc9cfa73b748150e4344218c98e4b8b651d23260b9515805cf695a3f553e3796bd38
6
+ metadata.gz: 2b9a904a1309e6d111ecf4b5172a4f07b6c57acfb7a8b219c4b890a6e914fcdc7e6bc9b7da5aa88b67aca1bffe57de185be2fe2583ab8fea8dedf416f67c1100
7
+ data.tar.gz: defc7c18b96063f1e1a4e88513ba7cc780c0fbf66aecf6f5eda74084721e4325a29a3f0ea3a50fec453309833280daa2db39aa3d782c548b86ee737dac1b92fe
data/README.md CHANGED
@@ -1,15 +1,6 @@
1
1
  # Bristow
2
2
 
3
- Bristow is a Ruby framework for creating function-calling enabled agents that work with OpenAI's GPT models. It provides a simple way to expose Ruby functions to GPT, handle the function calling protocol, and manage conversations.
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
- # Chat with a single message
65
- response = storyteller.chat('Tell me a story about Cold War era Berlin') do |part|
66
- print part # Stream the response
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
- # Chat with multiple messages
70
- response = storyteller.chat([
71
- 'Tell me a story about Cold War era Berlin',
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
- location: String,
87
- unit: String
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
- # Your weather API call here
68
+ # Implement your application logic here
91
69
  { temperature: 22, unit: unit }
92
70
  end
93
71
 
94
- # Create an agent with these functions
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 |part|
103
- print part # Stream the response
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 agencies. Here's an example using the included supervisor agency:
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 |part|
134
- print part
112
+ ]) do |response_chunk|
113
+ print response_chunk
135
114
  end
136
115
  ```
137
116
 
138
- The supervisor will:
139
- 1. Understand the user's request
140
- 2. Choose the appropriate agent(s)
141
- 3. Delegate parts of the task to different agents
142
- 4. Combine their responses into a coherent answer
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
@@ -20,5 +20,4 @@ messages = agency.chat([
20
20
  print part
21
21
  end
22
22
 
23
-
24
- puts messages
23
+ puts messages
@@ -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
- location: String,
13
- unit: String
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 messages_from_chat
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
@@ -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
@@ -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
- validate_parameters!(kwargs)
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 validate_parameters!(kwargs)
35
- # Check for missing required parameters
36
- missing = required_parameters - kwargs.keys
37
- raise ArgumentError, "missing keyword: :#{missing.first}" if missing.any?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bristow
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.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-22 00:00:00.000000000 Z
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