bristow 0.2.1 → 0.3.1

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: 4a5ea707c57046701bd21f3ab1b286844084f4f537363a6bd8db314c9508dd4e
4
- data.tar.gz: f58d69fd68907482a59aa5322be3981ce6ecf696f0721106bd416fded84a9628
3
+ metadata.gz: 83bcff1d07e84dc62048bb20b5efbde10141e5c453b055bbef3d8ca38b5cd2a6
4
+ data.tar.gz: 006d1be92b480065c507bfd5045c6f504c640e01bf8977c07f133059a4f56b50
5
5
  SHA512:
6
- metadata.gz: 9c1d8e1fff1d361cb58e1f35c05c81271c791d81c38315d6be8d79ccbbe2228fcf9e48ad8ccd6ffb119e6c92bcfb29587627e385d8a6ed7405f1b7e926afcd4e
7
- data.tar.gz: 7ff6f846b72d9fd93930583386de052e0e1d27774ebc35e632ee69d53c306fead5841ec130abdbff2f3c8b6c315ee0092d8fba373dbe7856bb341c216d2d75d8
6
+ metadata.gz: '019da82b66875e737c376f4f4284a7f81ee7991fe8da8fa5da913954b372c107f99b44773e21eacf307be3fe30bbdc13e9fd68a1536d65ca2fd71e47c2fd62a4'
7
+ data.tar.gz: e82a99a0311273a57ae882777a9afa1905fedea747ea9594ae38d4d2ba3163833d5c8b854ca475bf81eecac6acd718864461c29a1886d5f1bc74df0e0ef5fed9
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,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-02-01
4
+
5
+ - Dropped errant require of 'debug'
6
+
7
+ ## [0.3.0] - 2025-02-01
8
+
9
+ - Major update to the API. Now primarily class based.
10
+ Check the README for new details.
11
+
12
+ - Require updating CHANGELOG.md in each PR, but only in PRs.
13
+
3
14
  ## [0.2.1] - 2025-01-29
4
15
 
5
16
  - Fixed schema for supervisor agent function call
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,56 +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
- 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
- }
59
- }
60
- ) do |agent_name:, message:|
61
- raise "No agency set for supervisor" unless agency
62
-
63
- if agent_name == name
64
- { error: "Cannot delegate to self" }
65
- else
66
- agent = agency.find_agent(agent_name)
67
- raise ArgumentError, "Agent #{agent_name} not found" unless agent
68
-
69
- Bristow.configuration.logger.info("Delegating to #{agent_name}: #{message}")
70
- response = agent.chat([{ "role" => "user", "content" => message }])
71
- { response: response.last["content"] }
72
- end
73
- end
74
- end
75
55
  end
76
56
  end
77
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.1"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/bristow.rb CHANGED
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
+ require 'openai'
3
+ require 'logger'
4
+ require 'json'
2
5
 
3
6
  require_relative "bristow/version"
7
+ require_relative "bristow/helpers/sgetter"
8
+ require_relative "bristow/helpers/delegate"
4
9
  require_relative "bristow/configuration"
5
10
  require_relative "bristow/function"
11
+ require_relative "bristow/functions/delegate"
6
12
  require_relative "bristow/agent"
7
13
  require_relative "bristow/agents/supervisor"
8
14
  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.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hampton
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-30 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