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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e61544a71407814e6c09e22d56631df8ed7a0475cf8b182ba4d7bf86bcf68d4c
4
- data.tar.gz: 29a5285d24236fbd6226bffebbdd34becd3a7c2ac81c33fafde0a35d0e8eaf31
3
+ metadata.gz: 41e9fcfe099defc5696c4ef49b8bce8b63091de906e7cd7558a9e89a98734360
4
+ data.tar.gz: c19e7c5f20f658d8bc9e669513a9df4f730bc8a6595009866d096011d85f0751
5
5
  SHA512:
6
- metadata.gz: 2b9a904a1309e6d111ecf4b5172a4f07b6c57acfb7a8b219c4b890a6e914fcdc7e6bc9b7da5aa88b67aca1bffe57de185be2fe2583ab8fea8dedf416f67c1100
7
- data.tar.gz: defc7c18b96063f1e1a4e88513ba7cc780c0fbf66aecf6f5eda74084721e4325a29a3f0ea3a50fec453309833280daa2db39aa3d782c548b86ee737dac1b92fe
6
+ metadata.gz: fa99a514a81713700df0e78f8dafedd26e49e511edb1e2fd4c83a56ad6310c082d05cf61f7183bd8c00f4844f94632251f1bb6db098e7b18a04e213e0c9454ac
7
+ data.tar.gz: 16ab56ba600e68525b5c12dd7186c9aa1b2d3be28913369a6b1eb50f8e14bf34daba9188dd9c77540de2eb0da1fa0a4bd48889c85f502a610d0b19883faa7581
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
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
- storyteller = Bristow::Agent.new(
31
- name: 'Sydney',
32
- description: 'Agent for telling spy stories',
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
- weather = Bristow::Function.new(
51
- name: "get_weather",
52
- description: "Get the current weather for a location",
53
- parameters: {
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
- ) do |location:, unit: 'celsius'|
68
- # Implement your application logic here
69
- { temperature: 22, unit: unit }
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
- weather_agent = Bristow::Agent.new(
74
- name: "WeatherAssistant",
75
- description: "Helps with weather-related queries",
76
- functions: [weather]
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
- weather_agent.chat("What's the weather like in London?") do |response_chunk|
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
- ### Multi-Agent System
89
-
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:
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
- ```ruby
93
- # Create specialized agents. These can be configured with functions, as well.
94
- pirate_talker = Bristow::Agent.new(
95
- name: "PirateSpeaker",
96
- description: "Agent for translating input to pirate-speak",
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
- travel_agent = Bristow::Agent.new(
101
- name: "TravelAgent",
102
- description: "Agent for planning trips",
103
- system_message: 'Given a destination, you will respond with a detailed itenerary that includes only dates, times, and locations.',
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 a supervisor agency to coordinate the agents
107
- agency = Bristow::Agencies::Supervisor.create(agents: [pirate_talker, travel_agent])
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
- # The supervisor will automatically delegate to the appropriate agent as needed before generating a response for the user.
110
- agency.chat([
111
- { role: "user", content: "I want to go to New York. Tell me about it as if you were a pirate." }
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.default_model = 'gpt-4o'
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 "../lib/bristow"
1
+ require_relative '../lib/bristow'
2
2
 
3
- pirate_talker = Bristow::Agent.new(
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
- )
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
- agency = Bristow::Agencies::Supervisor.create(agents: [pirate_talker, travel_agent])
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
- messages = agency.chat([
18
- { role: "user", content: "I want to go to New York. Please plan my trip and tell me about it as if you were a pirate." }
19
- ]) do |part|
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
- puts messages
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
@@ -1,15 +1,22 @@
1
- require_relative "../lib/bristow"
1
+ require_relative '../lib/bristow'
2
2
 
3
3
  Bristow.configure do |config|
4
- config.default_model = 'gpt-4o-mini'
4
+ config.model = 'gpt-4o-mini'
5
5
  end
6
6
 
7
- sydney = Bristow::Agent.new(
8
- name: 'Sydney',
9
- description: 'Agent for telling spy stories',
10
- system_message: 'Given a topic, you will tell a brief spy story',
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
- sydney.chat([{role: 'user', content: 'Cold war era Berlin'}]) do |part|
14
- print part
15
- end
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
@@ -1,14 +1,14 @@
1
- require_relative "../lib/bristow"
1
+ require_relative '../lib/bristow'
2
2
 
3
3
  Bristow.configure do |config|
4
- config.default_model = 'gpt-4o-mini'
4
+ config.model = 'gpt-4o-mini'
5
5
  end
6
6
 
7
7
  # Define functions that GPT can call
8
- weather = Bristow::Function.new(
9
- name: "get_weather",
10
- description: "Get the current weather for a location",
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
- ) do |location:, unit: 'celsius'|
27
- # Your weather API call here
28
- { temperature: 22, unit: unit }
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
- weather_agent = Bristow::Agent.new(
33
- name: "WeatherAssistant",
34
- description: "Helps with weather-related queries",
35
- functions: [weather]
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
- attr_accessor :supervisor
4
+ attr_reader :supervisor
5
5
 
6
- def self.create(agents: [])
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 = nil # Will be set by create
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 "No supervisor set" unless supervisor
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
@@ -1,17 +1,26 @@
1
1
  module Bristow
2
2
  class Agency
3
- attr_reader :agents
3
+ include Bristow::Sgetter
4
4
 
5
- def initialize(agents: [])
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, "Agency#chat must be implemented by a subclass"
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.name == name }
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
- attr_reader :name, :description, :functions, :system_message, :chat_history
8
-
9
- def initialize(name:, description:, system_message: nil, functions: [], model: Bristow.configuration.default_model)
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 do |function|
28
- function.to_openai_schema
29
- end
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: @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 = @client.chat(parameters: params)
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
- @logger.error("Error calling OpenAI API: #{e.response[:body]}")
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
- @client.chat(parameters: params)
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
- attr_accessor :agency
5
-
6
- def initialize(available_agents:, agency: nil, name: "Supervisor", description: "A supervisor agent that coordinates between specialized agents", model: Bristow.configuration.default_model)
7
- super(
8
- name: name,
9
- description: description,
10
- system_message: build_system_message(available_agents),
11
- functions: [delegate_function],
12
- model: model
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
- You are a supervisor agent that coordinates between specialized agents.
26
- Your role is to:
27
- 1. Understand the user's request
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, :default_model, :logger
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
- @default_model = 'gpt-4o-mini'
8
+ @model = 'gpt-4o-mini'
12
9
  @logger = Logger.new(STDOUT)
13
10
  reset_client
14
11
  end
@@ -2,39 +2,42 @@
2
2
 
3
3
  module Bristow
4
4
  class Function
5
- attr_reader :name, :description, :parameters
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
- def call(**kwargs)
20
- validate_required_parameters!(kwargs)
21
- @block.call(**kwargs)
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
- raise ArgumentError, "missing keyword: #{missing.first}" if missing.any?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bristow
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.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-29 00:00:00.000000000 Z
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.2
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