Project

bristow

0.0
There's a lot of open issues
A long-lived project that still receives updates
Bristow provides a flexible framework for creating and managing systems of agents that can work together, hand off tasks between each other, and execute functions. Perfect for building complex AI systems and automation workflows.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

~> 13.0
~> 3.0
~> 0.22
~> 3.18
~> 1.10

Runtime

~> 1.0
~> 7.0.0
 Project Readme

Bristow

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.

Installation

Add this line to your application's Gemfile:

gem 'bristow'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install bristow

Quick start

The main ideas concepts of this gem are:

  • Agents: Basically an AI model wrapper with a baked in system prompt, instructing it what it should do.
  • Functions: code the AI model can call to work with data in your application.
  • 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.

Here's how you might define an agent that can lookup user information in your application to answer questions from an admin:

require 'bristow'

# Configure Bristow
Bristow.configure do |config|
  # Bristow will use ENV['OPENAI_API_KEY'] by default, but you can
  # set a custom OpenAI API key with:
  config.api_key = ENV['MY_OPENAI_KEY']
end

# Define functions that the model can call to interact with your app
class UserSearch < Bristow::Function
  # Name that will be provided to the AI model for function calls
  function_name "user_search"

  # Description for the AI model that it can use to determine when
  # it should call this function
  description "Allows searching for users by domain or email. You must provide at least one search param."

  # API of the function that will be provided to the model.
  # https://platform.openai.com/docs/guides/function-calling
  parameters({ 
    properties: {
      domain: {
        type: "string",
        description: "Search users by email domain"
      },
      email: {
        type: "string",
        description: "Search users by email address"
      }
    }
  })

  # The implementation of this function. The AI model can choose
  # whether or not to call this function, and this is the code that
  # will execute if it does. It's how the AI works with data in 
  # your application. 
  def perform(domain: nil, email: nil)
	query = { domain:, email: }.compact	
    return "You must specify either domain or email for the user search" if query.empty?
    
	User.where(query).to_json
  end
end

# Create an agent with access to the function
class UserQueryAssistant < Bristow::Agent
  agent_name "UserQueryAssistant"
  description "Helps with user-related queries"
  system_message <<~MSG 
    You are a user management assistant. 
    Given a task by an end user, will work on their behalf using function calls to accomplish this task.
  MSG
  functions [WeatherLookup]
end

# Chat with the agent
user_query_assistant = UserQueryAssistant.new
user_query_assistant.chat("Which users from Google have the fanciest sounding titles?") do |response_chunk|
  # As the agent streams the response to this block. 
  # This block with receive a few characters of the response
  # at a time as the response streams from the API.  
  print response_chunk 
end

Agents

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:

conversation = UserQueryAssistant.new.chat('Who is the CEO of Google?')

# 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:
#  - Original system prompt
#  - The original user query
#  - Any function calls made, and the responses to those function calls
#  - The final response to the user
puts conversation 

# 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:
UserQueryAssistant.new.chat('Who is the CEO of Google?') do |text_chunk|
  # This block will be called many times while the final user response 
  # is being generated. In each call, `text_chunk` will contain the next
  # few characters of the response, which you can render for your user.
  puts text_chunk
end

Agent configuration

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.

Basic agent definition

class Pirate < Bristow::Agent
  agent_name "Pirate"
  description "An agent that assists the user while always talking like a pirate."
  system_message "You are a helpful assistant that always talks like a pirate. Try to be as corny and punny as possible."
end

Pirate.new.chat("What's the best way to get from New York to Miami?") do |chunk|
  puts chunk # => "Ahoy matey! If ye be lookin' to sail the seas from..."
end

Agent config options

Here's an overview of all config options available when configuring an Agent:

  • agent_name: The name of the agent
  • 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.
  • 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.
  • 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.
  • model: The AI model to use. Defaults to Bristow.configuration.model.
  • client: The client to use. Defaults to Bristow.configuration.client.
  • logger: The logger class to use when logging debug information. Defaults to Bristow.configuration.logger.

When instantiating an instance of an agent, you can override these options for a specific instaces like this:

class Pirate < Bristow::Agent
  ...
end

regular_pirate = Pirate.new
smart_pirate = Pirate.new(model: 'o3')

Functions

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.

Basic agent definition

class TodoAssigner < Bristow::Function
  function_name "todo_assigner"
  description "Given a user ID and a todo ID, it will assign the todo to the user."
  parameters({
    properties: {
      user_id: {
        type: "string",
        description: "ID of the user the todo should be assigned to"
      },
      todo_id: {
        type: "string",
        description: "ID of the todo to assign"
      },
      reason: {
        type: "string",
        description: "Why you decided to assign this todo to the user"
      }
    },
    required: ["user_id", "todo_id"]
  })

  def perform(user_id:, todo_id:, reason: '')
    TodoUsers.create( user_id:, todo_id:, reason:)  
  end
end

Function config options

Functions have 3 config options, and a #perform function:

  • 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.
  • 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.
  • parameters: The JSON schema definition of the function's API. See Open AI's function docs for detailed information.
  • #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.

Agencies

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 in Ruby.

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.

The base agency only has 3 items in it's public API:

  • agents: A config option that holds the list of Agents it can work with.
  • #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.
  • #find_agent(name): A helper function for facilitating hand-offs between agents.

Agency definition

# We'll name this agency Sterling Cooper to
# keep in line with our out of date TV show
# reference naming convention.
class SterlingCooper < Agency
  agents [DonDraper, PeggyOlson, PeteCampbell]

  def chat(messages, &block)
    # Here's where you'd implement the multi-agent patternn
    # In this example, we'll just do a workflow, where we
    # loop through each agent and allow them to respond once.
    # This sort of simple workflow could work if you want a
    # specific set of steps to be repeated every time.
    agents.each do |agent|
	    messages = agent.chat(messages, &block)
	end

	messages
  end
end

campaign = SterlingCooper.new.chat("Please come up with an ad campaign for the Bristow gem")

Pre-packaged agencies

Supervisor Agency Overview

The supervisor agency implements a pattern something like LangChain's Multi-agent supervisor pattern. You provide an array of agents, and a pre-packaged Supervisor agent will handle:

  1. Receive the task from the user
  2. Analyze which agent you've provided might be best suited to handle the next step of the work
  3. Call that agent
  4. Repeat agent calls until it believes the task is complete
  5. Craft a final answer to the end user

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.

You can see examples/basic_agency.rb for example code.

Worfkflow Agency Overview

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:

  1. Receive the task from the user
  2. Call each agent in order
  3. Stream the response from the last agent in the series

You can see examples/workflow_agency.rb for example code.

Examples

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.

Configuration

Configure Bristow with your settings:

Bristow.configure do |config|
  # Your OpenAI API key (defaults to ENV['OPENAI_API_KEY'])
  config.openai_api_key = 'your-api-key'
  
  # The default model to use (defaults to 'gpt-4o-mini')
  config.model = 'gpt-4o'
  
  # Logger to use (defaults to Logger.new(STDOUT))
  config.logger = Rails.logger
end

# You can overrided these settings on a per-agent basis like this:
storyteller = Bristow::Agent.new(
  agent_name: 'Sydney',
  description: 'Agent for telling spy stories',
  system_message: 'Given a topic, you will tell a brief spy story',
  model: 'gpt-4o-mini',
  logger: Logger.new(STDOUT)
)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/andrewhampton/bristow. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Bristow project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.