bristow 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83bcff1d07e84dc62048bb20b5efbde10141e5c453b055bbef3d8ca38b5cd2a6
4
- data.tar.gz: 006d1be92b480065c507bfd5045c6f504c640e01bf8977c07f133059a4f56b50
3
+ metadata.gz: 2ef9882c1bac79c842354a58c175fd1ed1c6228140c2139246dcdc22bb693728
4
+ data.tar.gz: 4f7c34334cda242b9a7dd1f34072df637aebb3d0e59ed06d2dd665219c44d210
5
5
  SHA512:
6
- metadata.gz: '019da82b66875e737c376f4f4284a7f81ee7991fe8da8fa5da913954b372c107f99b44773e21eacf307be3fe30bbdc13e9fd68a1536d65ca2fd71e47c2fd62a4'
7
- data.tar.gz: e82a99a0311273a57ae882777a9afa1905fedea747ea9594ae38d4d2ba3163833d5c8b854ca475bf81eecac6acd718864461c29a1886d5f1bc74df0e0ef5fed9
6
+ metadata.gz: 3333d030e4db031098a26c6d1ddea67ba6c23a42b58b4f90ecab5289f6565c230b8a9e10becb4c9af11c18198da66fb5b1be6174b43c1ffc4350b5b1e19ba7f0
7
+ data.tar.gz: 75d286829802af2c80ba1a7638d1b27129aedf66d5e331195cffc942caeb1272eb5b5d317538aa359d2db9b627320c0e74a1c2d8e4867cf77bba7b43c12021e2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-02-15
4
+
5
+ - Update the README to make it flow a bit better.
6
+ - Add .chat to Agent
7
+ - Give functions a default name that is the name of the class
8
+ - Add the workflow agency
9
+
3
10
  ## [0.3.1] - 2025-02-01
4
11
 
5
12
  - 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
+ 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
+ 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
+ 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
+ - `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
+ 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
+ - `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.
@@ -0,0 +1,27 @@
1
+ require_relative '../lib/bristow'
2
+
3
+ class TravelAgent < Bristow::Agent
4
+ 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
+ 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
@@ -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
@@ -5,7 +5,7 @@ module Bristow
5
5
  include Bristow::Sgetter
6
6
  include Bristow::Delegate
7
7
 
8
- sgetter :name
8
+ sgetter :name, default: -> { self.class.name }
9
9
  sgetter :description
10
10
  sgetter :parameters, default: {}
11
11
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bristow
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/bristow.rb CHANGED
@@ -13,6 +13,7 @@ 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"
16
17
 
17
18
  module Bristow
18
19
  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.4.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: 2025-02-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ruby-openai
@@ -123,11 +123,13 @@ files:
123
123
  - LICENSE.txt
124
124
  - README.md
125
125
  - Rakefile
126
- - examples/agency.rb
126
+ - examples/basic_agency.rb
127
127
  - examples/basic_agent.rb
128
128
  - examples/function_calls.rb
129
+ - examples/workflow_agency.rb
129
130
  - lib/bristow.rb
130
131
  - lib/bristow/agencies/supervisor.rb
132
+ - lib/bristow/agencies/workflow.rb
131
133
  - lib/bristow/agency.rb
132
134
  - lib/bristow/agent.rb
133
135
  - lib/bristow/agents/supervisor.rb
File without changes