bristow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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