bristow 0.3.1 → 0.5.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: 83bcff1d07e84dc62048bb20b5efbde10141e5c453b055bbef3d8ca38b5cd2a6
4
- data.tar.gz: 006d1be92b480065c507bfd5045c6f504c640e01bf8977c07f133059a4f56b50
3
+ metadata.gz: 1888aaf7dc60df075779ec15107d64b7b611f979821843690f9c55e6cae522be
4
+ data.tar.gz: 23e2cd997e1855c5060ceb08c5a5f64575ad0d68de046a6ddaa20bb1063fd48a
5
5
  SHA512:
6
- metadata.gz: '019da82b66875e737c376f4f4284a7f81ee7991fe8da8fa5da913954b372c107f99b44773e21eacf307be3fe30bbdc13e9fd68a1536d65ca2fd71e47c2fd62a4'
7
- data.tar.gz: e82a99a0311273a57ae882777a9afa1905fedea747ea9594ae38d4d2ba3163833d5c8b854ca475bf81eecac6acd718864461c29a1886d5f1bc74df0e0ef5fed9
6
+ metadata.gz: 8413b8e49219c72ff68cd8870139843506c6d29737c6defaa8ccb7f67a60dd2c39ac29f092a56e0c3df55f8218962dbbfa5e94596c64cc7610472c5667eff789
7
+ data.tar.gz: f9f39a0ad386ba9a025deb0c98d994e46c9e043d06f0ac4208c7a4e1b1c5f37a7bdc195baa83fa1ed5167141fac028cc4925eaee3ffb0ce8fb52cad7b362f273
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2025-05-08
4
+
5
+ - Add termination conditions so you can ensure an agent stop after a certain amount of time.
6
+ - Drop VCR in favor of WebMock for testing
7
+ - Fix `name` shadowing the built-in function for agent and function
8
+
9
+ ## [0.4.0] - 2025-02-15
10
+
11
+ - Update the README to make it flow a bit better.
12
+ - Add .chat to Agent
13
+ - Give functions a default name that is the name of the class
14
+ - Add the workflow agency
15
+
3
16
  ## [0.3.1] - 2025-02-01
4
17
 
5
18
  - Dropped errant require of 'debug'
data/README.md CHANGED
@@ -18,107 +18,254 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install bristow
20
20
 
21
- ## Usage
21
+ ## Quick start
22
+
23
+ The main ideas concepts of this gem are:
24
+
25
+ - **Agents**: Basically an AI model wrapper with a baked in system prompt, instructing it what it should do.
26
+ - **Functions**: code the AI model can call to work with data in your application.
27
+ - **Agencies**: Systems of agents working together to accomplish a task for a user. A chat call to an agency may call any number of agents within the agency to accomplish the task defined by the user.
28
+
29
+ Here's how you might define an agent that can lookup user information in your application to answer questions from an admin:
22
30
 
23
31
  ```ruby
24
32
  require 'bristow'
25
33
 
26
34
  # Configure Bristow
27
35
  Bristow.configure do |config|
28
- config.api_key = ENV['OPENAI_API_KEY']
36
+ # Bristow will use ENV['OPENAI_API_KEY'] by default, but you can
37
+ # set a custom OpenAI API key with:
38
+ config.api_key = ENV['MY_OPENAI_KEY']
29
39
  end
30
40
 
31
- # Define functions that the model can call
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 {
41
+ # Define functions that the model can call to interact with your app
42
+ class UserSearch < Bristow::Function
43
+ # Name that will be provided to the AI model for function calls
44
+ function_name "user_search"
45
+
46
+ # Description for the AI model that it can use to determine when
47
+ # it should call this function
48
+ description "Allows searching for users by domain or email. You must provide at least one search param."
49
+
50
+ # API of the function that will be provided to the model.
51
+ # https://platform.openai.com/docs/guides/function-calling
52
+ parameters({
37
53
  properties: {
38
- location: {
54
+ domain: {
39
55
  type: "string",
40
- description: "The city and state, e.g. San Francisco, CA"
56
+ description: "Search users by email domain"
41
57
  },
42
- unit: {
58
+ email: {
43
59
  type: "string",
44
- enum: ["celsius", "fahrenheit"],
45
- description: "The unit of temperature to return"
60
+ description: "Search users by email address"
46
61
  }
47
- },
48
- required: ["location"]
49
- }
50
-
51
- def perform(location:, unit: 'celsius')
52
- # Implement your application logic here
53
- { temperature: 22, unit: unit }
62
+ }
63
+ })
64
+
65
+ # The implementation of this function. The AI model can choose
66
+ # whether or not to call this function, and this is the code that
67
+ # will execute if it does. It's how the AI works with data in
68
+ # your application.
69
+ def perform(domain: nil, email: nil)
70
+ query = { domain:, email: }.compact
71
+ return "You must specify either domain or email for the user search" if query.empty?
72
+
73
+ User.where(query).to_json
54
74
  end
55
75
  end
56
76
 
57
77
  # Create an agent with access to the function
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."
78
+ class UserQueryAssistant < Bristow::Agent
79
+ agent_name "UserQueryAssistant"
80
+ description "Helps with user-related queries"
81
+ system_message <<~MSG
82
+ You are a user management assistant.
83
+ Given a task by an end user, will work on their behalf using function calls to accomplish this task.
84
+ MSG
62
85
  functions [WeatherLookup]
63
86
  end
64
87
 
65
88
  # Chat with the agent
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
89
+ user_query_assistant = UserQueryAssistant.new
90
+ user_query_assistant.chat("Which users from Google have the fanciest sounding titles?") do |response_chunk|
91
+ # As the agent streams the response to this block.
92
+ # This block with receive a few characters of the response
93
+ # at a time as the response streams from the API.
69
94
  print response_chunk
70
95
  end
96
+ ```
97
+
98
+ # Agents
99
+
100
+ Agents provide you with an easy way to make calls to OpenAI. Once you've written the agent class, you can use it by calling `#chat` on any instance. The response from the AI is provided in two ways:
101
+
102
+ ```ruby
103
+ conversation = UserQueryAssistant.new.chat('Who is the CEO of Google?')
104
+
105
+ # At this point, `conversation` will contain the entire conversation history UserQueryAssistant had to work on the task on behalf of the user. This will include:
106
+ # - Original system prompt
107
+ # - The original user query
108
+ # - Any function calls made, and the responses to those function calls
109
+ # - The final response to the user
110
+ puts conversation
111
+
112
+ # You can also provide a block to #chat that will stream only the final response to the user as it is generated by the AI:
113
+ UserQueryAssistant.new.chat('Who is the CEO of Google?') do |text_chunk|
114
+ # This block will be called many times while the final user response
115
+ # is being generated. In each call, `text_chunk` will contain the next
116
+ # few characters of the response, which you can render for your user.
117
+ puts text_chunk
118
+ end
119
+ ```
120
+
121
+ ## Agent configuration
122
+
123
+ Agents can be configured similar to something like `ActiveJob` or `Sidekiq`. You inherit from `Bristow::Agent` then call some helpers that will set the default config values. These defaults can be overridden when instantiating.
124
+
125
+ ### Basic agent definition
126
+
127
+ ```ruby
128
+ class Pirate < Bristow::Agent
129
+ agent_name "Pirate"
130
+ description "An agent that assists the user while always talking like a pirate."
131
+ system_message "You are a helpful assistant that always talks like a pirate. Try to be as corny and punny as possible."
132
+ end
133
+
134
+ Pirate.new.chat("What's the best way to get from New York to Miami?") do |chunk|
135
+ puts chunk # => "Ahoy matey! If ye be lookin' to sail the seas from..."
136
+ end
137
+
138
+ ```
139
+
140
+ ### Agent config options
141
+
142
+ Here's an overview of all config options available when configuring an Agent:
143
+
144
+ - `agent_name`: The name of the agent
145
+ - `description`: Description of what the agent can do. Can be used by agencies to provide information about the agent, informing the model when this agent should be used.
146
+ - `system_message`: The system message to be sent before the first user message. This can be used to provide context to the model about the conversation.
147
+ - `functions`: An array of `Bristow::Function` classes that the agent has access to. When working on the task assigned by the user, the AI model will have access to these functions, and will decide when a call to any function call is necessary.
148
+ - `model`: The AI model to use. Defaults to `Bristow.configuration.model`.
149
+ - `client`: The client to use. Defaults to `Bristow.configuration.client`.
150
+ - `logger`: The logger class to use when logging debug information. Defaults to `Bristow.configuration.logger`.
151
+
152
+ When instantiating an instance of an agent, you can override these options for a specific instaces like this:
153
+
154
+ ```ruby
155
+ class Pirate < Bristow::Agent
156
+ ...
157
+ end
158
+
159
+ regular_pirate = Pirate.new
160
+ smart_pirate = Pirate.new(model: 'o3')
161
+ ```
162
+
163
+ # Functions
71
164
 
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 {
165
+ You can think of functions as an API for your application for the AI model. When responding to a user's request, the AI model may respond directly, or choose to call functions you provide.
166
+
167
+ ### Basic agent definition
168
+
169
+ ```ruby
170
+ class TodoAssigner < Bristow::Function
171
+ function_name "todo_assigner"
172
+ description "Given a user ID and a todo ID, it will assign the todo to the user."
173
+ parameters({
77
174
  properties: {
78
- location: {
175
+ user_id: {
79
176
  type: "string",
80
- description: "The city and state, e.g. San Francisco, CA"
177
+ description: "ID of the user the todo should be assigned to"
81
178
  },
82
- temperature: {
83
- type: "number",
84
- description: "The temperature in the specified unit"
179
+ todo_id: {
180
+ type: "string",
181
+ description: "ID of the todo to assign"
85
182
  },
86
- unit: {
183
+ reason: {
87
184
  type: "string",
88
- enum: ["celsius", "fahrenheit"],
89
- description: "The unit of temperature"
185
+ description: "Why you decided to assign this todo to the user"
90
186
  }
91
187
  },
92
- required: ["location", "temperature", "unit"]
93
- }
188
+ required: ["user_id", "todo_id"]
189
+ })
94
190
 
95
- def self.perform(location:, temperature:, unit:)
96
- # Store the weather data in your database
97
- { status: "success", message: "Weather data stored for #{location}" }
191
+ def perform(user_id:, todo_id:, reason: '')
192
+ TodoUsers.create( user_id:, todo_id:, reason:)
98
193
  end
99
194
  end
195
+ ```
100
196
 
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
197
+ ### Function config options
107
198
 
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
199
+ Functions have 3 config options, and a `#perform` function:
200
+
201
+ - `function_name`: The name of the function. Provided to the AI model to help it call this function, and can be used to determine whether or not this function should be called.
202
+ - `description`: A description of the function that will be provided to the AI model. Important for informing the model about what this function can do, so it's able to determine when to call this function.
203
+ - `parameters`: The JSON schema definition of the function's API. See Open AI's [function docs](https://platform.openai.com/docs/guides/function-calling) for detailed information.
204
+ - `#perform`: The perform function is your implementation for the function. You'll check out the parameters passed in from the model, and handle any operation it's requesting to do.
205
+
206
+ # Agencies
207
+
208
+ Agencies are sets of agents that can work together on a task. It's how we can implement something like AutoGen's [multi-agent design patterns](https://microsoft.github.io/autogen/stable/user-guide/core-user-guide/design-patterns/intro.html) in Ruby.
209
+
210
+ There've very little going on in the base agent class. The long term goal of this gem is to pre-package common patterns. We currently only have the Supervisor pattern pre-packaged. It's probably best to start there. However, once you're ready to build your own multi-agent pattern, here's what you need to know.
114
211
 
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|
118
- print response_chunk
212
+ The base agency only has 3 items in it's public API:
213
+
214
+ - `agents`: A config option that holds the list of `Agents` it can work with.
215
+ - `#chat`: The chat method that is the entry point for the user's task. It raises a `NotImplementedError` in the base class, so it's up to you to implement the interaction pattern unless you want to use a pre-packaged agency.
216
+ - `#find_agent(name)`: A helper function for facilitating hand-offs between agents.
217
+ ### Agency definition
218
+
219
+ ```ruby
220
+ # We'll name this agency Sterling Cooper to
221
+ # keep in line with our out of date TV show
222
+ # reference naming convention.
223
+ class SterlingCooper < Agency
224
+ agents [DonDraper, PeggyOlson, PeteCampbell]
225
+
226
+ def chat(messages, &block)
227
+ # Here's where you'd implement the multi-agent patternn
228
+ # In this example, we'll just do a workflow, where we
229
+ # loop through each agent and allow them to respond once.
230
+ # This sort of simple workflow could work if you want a
231
+ # specific set of steps to be repeated every time.
232
+ agents.each do |agent|
233
+ messages = agent.chat(messages, &block)
234
+ end
235
+
236
+ messages
237
+ end
119
238
  end
239
+
240
+ campaign = SterlingCooper.new.chat("Please come up with an ad campaign for the Bristow gem")
120
241
  ```
121
242
 
243
+ ## Pre-packaged agencies
244
+
245
+ ### Supervisor Agency Overview
246
+
247
+ The supervisor agency implements a pattern something like [LangChain's Multi-agent supervisor pattern](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/). You provide an array of agents, and a pre-packaged Supervisor agent will handle:
248
+
249
+ 1. Receive the task from the user
250
+ 2. Analyze which agent you've provided might be best suited to handle the next step of the work
251
+ 3. Call that agent
252
+ 4. Repeat agent calls until it believes the task is complete
253
+ 5. Craft a final answer to the end user
254
+
255
+ This can be useful when building a chat bot for your application. You can build out the agents and functions that interact with different parts of your system, a reporting agent, a user management agent, etc. You then throw them all together in in a supervisor agency, and expose a chat UI for admins. This chat UI would then be allow the AI model to interact with your application.
256
+
257
+ You can see `examples/basic_agency.rb` for example code.
258
+
259
+ ### Worfkflow Agency Overview
260
+
261
+ The workflow agency is a simple pattern that allows you to define a set of steps that should be repeated every time a user interacts with the agency. This can be useful when you have a specific set of steps that should be repeated every time a user interacts with the agency. It will:
262
+
263
+ 1. Receive the task from the user
264
+ 2. Call each agent in order
265
+ 3. Stream the response from the last agent in the series
266
+
267
+ You can see `examples/workflow_agency.rb` for example code.
268
+
122
269
  ## Examples
123
270
 
124
271
  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.
@@ -141,7 +288,7 @@ end
141
288
 
142
289
  # You can overrided these settings on a per-agent basis like this:
143
290
  storyteller = Bristow::Agent.new(
144
- name: 'Sydney',
291
+ agent_name: 'Sydney',
145
292
  description: 'Agent for telling spy stories',
146
293
  system_message: 'Given a topic, you will tell a brief spy story',
147
294
  model: 'gpt-4o-mini',
@@ -1,13 +1,13 @@
1
1
  require_relative '../lib/bristow'
2
2
 
3
3
  class PirateTalker < Bristow::Agent
4
- name "PirateSpeaker"
4
+ agent_name "PirateSpeaker"
5
5
  description "Agent for translating input to pirate-speak"
6
6
  system_message 'Given a text, you will translate it to pirate-speak.'
7
7
  end
8
8
 
9
9
  class TravelAgent < Bristow::Agent
10
- name "TravelAgent"
10
+ agent_name "TravelAgent"
11
11
  description "Agent for planning trips"
12
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
13
  end
@@ -5,7 +5,7 @@ Bristow.configure do |config|
5
5
  end
6
6
 
7
7
  class Sydney < Bristow::Agent
8
- name 'Sydney'
8
+ agent_name 'Sydney'
9
9
  description 'Agent for telling spy stories'
10
10
  system_message 'Given a topic, you will tell a brief spy story'
11
11
  end
@@ -0,0 +1,29 @@
1
+ require_relative '../lib/bristow'
2
+
3
+ class CountAgent < Bristow::Agent
4
+ agent_name "CounterAgent"
5
+ description "Knows how to count"
6
+ system_message "You are a helpful mupet vampire that knows how to count very well. You will find the last message in the series and reply with the next integer."
7
+ termination Bristow::Terminations::MaxMessages.new(3)
8
+ end
9
+
10
+ fake_history = [
11
+ { role: "user", content: "What comes after 3" },
12
+ { role: "assistant", content: "4" },
13
+ { role: "user", content: "What comes after 4" },
14
+ { role: "assistant", content: "5" },
15
+ { role: "user", content: "What comes after 5" },
16
+ { role: "assistant", content: "6" }
17
+ ]
18
+
19
+ # Start a conversation, but given the termination condition,
20
+ # there should be no more messages added. It should return
21
+ # immediately returning the provided input.
22
+ messages = CountAgent.chat(fake_history) do |part|
23
+ puts "This will never execute"
24
+ end
25
+
26
+ puts ''
27
+ puts '*' * 10
28
+ puts 'All messages:'
29
+ pp messages
@@ -6,7 +6,7 @@ end
6
6
 
7
7
  # Define functions that GPT can call
8
8
  class WeatherLookup < Bristow::Function
9
- name "get_weather"
9
+ function_name "get_weather"
10
10
  description "Get the current weather for a location"
11
11
  parameters ({
12
12
  type: "object",
@@ -32,7 +32,7 @@ end
32
32
 
33
33
  # Create an agent with these functions
34
34
  class WeatherAgent < Bristow::Agent
35
- name "WeatherAssistant"
35
+ agent_name "WeatherAssistant"
36
36
  description "Helps with weather-related queries"
37
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
38
  functions [WeatherLookup]
@@ -0,0 +1,27 @@
1
+ require_relative '../lib/bristow'
2
+
3
+ class TravelAgent < Bristow::Agent
4
+ agent_name "TravelAgent"
5
+ description "Agent for planning trips"
6
+ system_message 'Given a destination, you will plan a trip. You will respond with an itinerary that includes dates, times, and locations only.'
7
+ end
8
+
9
+ class StoryTeller < Bristow::Agent
10
+ agent_name "StoryTeller"
11
+ description 'An agent that tells a story given an agenda'
12
+ system_message "Given a trip agenda, you will tell a story about a traveler who recently took that trip. Be sure to highlight the traveler's experiences and emotions."
13
+ end
14
+
15
+ # The workflow agent implements basic workflow. It will call each agent in the
16
+ # order they are listed, passing the message history to each agent. The response
17
+ # from the last agent is streamed to the block passed to chat, but the entire
18
+ # message history is returned.
19
+ workflow = Bristow::Agencies::Workflow.new(agents: [TravelAgent, StoryTeller])
20
+
21
+ messages = workflow.chat('New York') do |part|
22
+ print part
23
+ end
24
+
25
+ puts '*' * 80
26
+ puts 'Chat history:'
27
+ pp messages
@@ -5,13 +5,15 @@ module Bristow
5
5
 
6
6
  sgetter :custom_instructions, default: nil
7
7
 
8
- def initialize(agents: self.class.agents.dup)
9
- @custom_instructions = self.class.custom_instructions
8
+ def initialize(agents: self.class.agents.dup, custom_instructions: self.class.custom_instructions, termination: Bristow::Terminations::MaxMessages.new(100))
9
+ @custom_instructions = custom_instructions
10
10
  @agents = agents
11
+ @termination = termination
11
12
  @supervisor = Agents::Supervisor.new(
12
13
  child_agents: agents,
13
14
  agency: self,
14
- custom_instructions: custom_instructions
15
+ custom_instructions: custom_instructions,
16
+ termination: termination
15
17
  )
16
18
  end
17
19
 
@@ -0,0 +1,16 @@
1
+ module Bristow
2
+ module Agencies
3
+ class Workflow < Agency
4
+ def chat(messages, &block)
5
+ return messages if agents.empty?
6
+
7
+ agents.each.with_index(1) do |agent, index|
8
+ last = index == agents.size
9
+ messages = agent.chat(messages, &(last ? block : nil))
10
+ end
11
+
12
+ messages
13
+ end
14
+ end
15
+ end
16
+ end
@@ -17,7 +17,7 @@ module Bristow
17
17
  end
18
18
 
19
19
  def find_agent(name)
20
- agent = agents.find { |agent| agent_name(agent) == name }
20
+ agent = agents.find { |agent| agent_name_for(agent) == name }
21
21
  return nil unless agent
22
22
 
23
23
  agent.is_a?(Class) ? agent.new : agent
@@ -32,8 +32,8 @@ module Bristow
32
32
 
33
33
  private
34
34
 
35
- def agent_name(agent)
36
- agent.is_a?(Class) ? agent.name : agent.class.name
35
+ def agent_name_for(agent)
36
+ agent.is_a?(Class) ? agent.agent_name : agent.class.agent_name
37
37
  end
38
38
  end
39
39
  end
data/lib/bristow/agent.rb CHANGED
@@ -2,25 +2,29 @@ module Bristow
2
2
  class Agent
3
3
  include Bristow::Sgetter
4
4
 
5
- sgetter :name
5
+ sgetter :agent_name
6
6
  sgetter :description
7
7
  sgetter :system_message
8
8
  sgetter :functions, default: []
9
9
  sgetter :model, default: -> { Bristow.configuration.model }
10
10
  sgetter :client, default: -> { Bristow.configuration.client }
11
11
  sgetter :logger, default: -> { Bristow.configuration.logger }
12
+ sgetter :termination, default: -> { Bristow::Terminations::MaxMessages.new(100) }
12
13
  attr_reader :chat_history
14
+
15
+
13
16
 
14
17
  def initialize(
15
- name: self.class.name,
16
- description: self.class.name,
18
+ agent_name: self.class.agent_name,
19
+ description: self.class.description,
17
20
  system_message: self.class.system_message,
18
21
  functions: self.class.functions.dup,
19
22
  model: self.class.model,
20
23
  client: self.class.client,
21
- logger: self.class.logger
24
+ logger: self.class.logger,
25
+ termination: self.class.termination
22
26
  )
23
- @name = name
27
+ @agent_name = agent_name
24
28
  @description = description
25
29
  @system_message = system_message
26
30
  @functions = functions
@@ -28,10 +32,11 @@ module Bristow
28
32
  @client = client
29
33
  @logger = logger
30
34
  @chat_history = []
35
+ @termination = termination
31
36
  end
32
37
 
33
38
  def handle_function_call(name, arguments)
34
- function = functions.find { |f| f.name == name }
39
+ function = functions.find { |f| f.function_name == name }
35
40
  raise ArgumentError, "Function #{name} not found" unless function
36
41
  function.call(**arguments.transform_keys(&:to_sym))
37
42
  end
@@ -53,7 +58,7 @@ module Bristow
53
58
 
54
59
  @chat_history = messages.dup
55
60
 
56
- loop do
61
+ while termination.continue?(messages) do
57
62
  params = {
58
63
  model: model,
59
64
  messages: messages
@@ -5,7 +5,7 @@ module Bristow
5
5
  class Supervisor < Agent
6
6
  attr_reader :agency
7
7
 
8
- name "Supervisor"
8
+ agent_name "Supervisor"
9
9
  description "A supervisor agent that coordinates between specialized agents"
10
10
  system_message <<~MESSAGE
11
11
  You are a supervisor agent that coordinates between specialized agents.
@@ -24,21 +24,23 @@ module Bristow
24
24
 
25
25
  sgetter :custom_instructions, default: nil
26
26
 
27
- def initialize(child_agents:, agency:, custom_instructions: nil)
27
+ def initialize(child_agents:, agency:, custom_instructions: nil, termination: self.class.termination)
28
28
  super()
29
29
  @custom_instructions = custom_instructions || self.class.custom_instructions
30
30
  @system_message = build_system_message(child_agents)
31
31
  @agency = agency
32
32
  @custom_instructions = custom_instructions || self.class.custom_instructions
33
+ @termination = termination
33
34
  agency.agents << self
34
- functions << Functions::Delegate.new(self, agency)
35
+ @functions ||= []
36
+ @functions << Functions::Delegate.new(self, agency)
35
37
  end
36
38
 
37
39
  private
38
40
 
39
41
  def build_system_message(available_agents)
40
42
  agent_descriptions = available_agents.map do |agent|
41
- "- #{agent.name}: #{agent.description}"
43
+ "- #{agent.agent_name}: #{agent.description}"
42
44
  end.join("\n")
43
45
 
44
46
  <<~MESSAGE
@@ -5,13 +5,23 @@ module Bristow
5
5
  include Bristow::Sgetter
6
6
  include Bristow::Delegate
7
7
 
8
- sgetter :name
8
+ sgetter :function_name, default: nil
9
9
  sgetter :description
10
10
  sgetter :parameters, default: {}
11
11
 
12
+ def initialize(
13
+ function_name: self.class.function_name,
14
+ description: self.class.description,
15
+ parameters: self.class.parameters
16
+ )
17
+ @function_name = function_name
18
+ @description = description
19
+ @parameters = parameters
20
+ end
21
+
12
22
  def self.to_openai_schema
13
23
  {
14
- name: name,
24
+ name: function_name,
15
25
  description: description,
16
26
  parameters: parameters
17
27
  }
@@ -3,9 +3,7 @@
3
3
  module Bristow
4
4
  module Functions
5
5
  class Delegate < Function
6
- def self.name
7
- "delegate_to"
8
- end
6
+ function_name "delegate_to"
9
7
 
10
8
  def self.description
11
9
  "Delegate a task to a specialized agent"
@@ -28,10 +26,6 @@ module Bristow
28
26
  }
29
27
  end
30
28
 
31
- def name
32
- self.class.name
33
- end
34
-
35
29
  def description
36
30
  self.class.description
37
31
  end
@@ -45,6 +39,7 @@ module Bristow
45
39
 
46
40
  @agent = agent
47
41
  @agency = agency
42
+ super()
48
43
  end
49
44
 
50
45
  def agency=(agency)
@@ -54,7 +49,7 @@ module Bristow
54
49
  def perform(agent_name:, message:)
55
50
  raise "Agency not set" if @agency.nil?
56
51
 
57
- if agent_name == @agent.name
52
+ if agent_name == @agent.agent_name
58
53
  { error: "Cannot delegate to self" }
59
54
  else
60
55
  agent = @agency.find_agent(agent_name)
@@ -4,16 +4,16 @@
4
4
  # Example:
5
5
  # class Agent
6
6
  # include Bristow::Sgetter
7
- # sgetter :name
7
+ # sgetter :agent_name
8
8
  # sgetter :model, default: -> { Bristow.configuration.model }
9
9
  # end
10
10
  #
11
11
  # class Sydney < Agent
12
- # name 'Sydney'
12
+ # agent_name 'Sydney'
13
13
  # end
14
14
  #
15
15
  # sydney = Sydney.new
16
- # sydney.name # => 'Sydney'
16
+ # sydney.agent_name # => 'Sydney'
17
17
  module Bristow
18
18
  module Sgetter
19
19
  def self.included(base)
@@ -0,0 +1,5 @@
1
+ class Bristow::Termination
2
+ def continue?(messages)
3
+ raise NotImplementedError, "`#continue?` must be implemented in #{self.class}"
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Bristow
2
+ module Terminations
3
+ class CanNotStopWillNotStop < Bristow::Termination
4
+ def continue?(_messages)
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Bristow
2
+ module Terminations
3
+ class MaxMessages < Bristow::Termination
4
+ def initialize(max_messages)
5
+ @max_messages = max_messages
6
+ end
7
+
8
+ def continue?(messages)
9
+ messages.size < @max_messages
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Bristow
2
+ module Terminations
3
+ class Timeout < Bristow::Termination
4
+ def initialize(end_time: Time.now + 60)
5
+ @end_time = end_time
6
+ end
7
+
8
+ def continue?(_messages)
9
+ Time.now < @end_time
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bristow
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/bristow.rb CHANGED
@@ -13,6 +13,11 @@ require_relative "bristow/agent"
13
13
  require_relative "bristow/agents/supervisor"
14
14
  require_relative "bristow/agency"
15
15
  require_relative "bristow/agencies/supervisor"
16
+ require_relative "bristow/agencies/workflow"
17
+ require_relative "bristow/termination"
18
+ require_relative "bristow/terminations/max_messages"
19
+ require_relative "bristow/terminations/timeout"
20
+ require_relative "bristow/terminations/can_not_stop_will_not_stop"
16
21
 
17
22
  module Bristow
18
23
  class Error < StandardError; 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.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hampton
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ruby-openai
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.0'
68
- - !ruby/object:Gem::Dependency
69
- name: vcr
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '6.0'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '6.0'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: webmock
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -123,11 +109,14 @@ files:
123
109
  - LICENSE.txt
124
110
  - README.md
125
111
  - Rakefile
126
- - examples/agency.rb
112
+ - examples/basic_agency.rb
127
113
  - examples/basic_agent.rb
114
+ - examples/basic_termination.rb
128
115
  - examples/function_calls.rb
116
+ - examples/workflow_agency.rb
129
117
  - lib/bristow.rb
130
118
  - lib/bristow/agencies/supervisor.rb
119
+ - lib/bristow/agencies/workflow.rb
131
120
  - lib/bristow/agency.rb
132
121
  - lib/bristow/agent.rb
133
122
  - lib/bristow/agents/supervisor.rb
@@ -136,6 +125,10 @@ files:
136
125
  - lib/bristow/functions/delegate.rb
137
126
  - lib/bristow/helpers/delegate.rb
138
127
  - lib/bristow/helpers/sgetter.rb
128
+ - lib/bristow/termination.rb
129
+ - lib/bristow/terminations/can_not_stop_will_not_stop.rb
130
+ - lib/bristow/terminations/max_messages.rb
131
+ - lib/bristow/terminations/timeout.rb
139
132
  - lib/bristow/version.rb
140
133
  homepage: https://github.com/andrewhampton/bristow
141
134
  licenses:
@@ -161,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
154
  - !ruby/object:Gem::Version
162
155
  version: '0'
163
156
  requirements: []
164
- rubygems_version: 3.6.3
157
+ rubygems_version: 3.6.8
165
158
  specification_version: 4
166
159
  summary: A Ruby framework for creating systems of cooperative AI agents with function
167
160
  calling capabilities