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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +208 -61
- data/examples/workflow_agency.rb +27 -0
- data/lib/bristow/agencies/workflow.rb +16 -0
- data/lib/bristow/function.rb +1 -1
- data/lib/bristow/version.rb +1 -1
- data/lib/bristow.rb +1 -0
- metadata +5 -3
- /data/examples/{agency.rb → basic_agency.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ef9882c1bac79c842354a58c175fd1ed1c6228140c2139246dcdc22bb693728
|
4
|
+
data.tar.gz: 4f7c34334cda242b9a7dd1f34072df637aebb3d0e59ed06d2dd665219c44d210
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
##
|
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
|
-
|
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
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
54
|
+
domain: {
|
39
55
|
type: "string",
|
40
|
-
description: "
|
56
|
+
description: "Search users by email domain"
|
41
57
|
},
|
42
|
-
|
58
|
+
email: {
|
43
59
|
type: "string",
|
44
|
-
|
45
|
-
description: "The unit of temperature to return"
|
60
|
+
description: "Search users by email address"
|
46
61
|
}
|
47
|
-
}
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
59
|
-
name "
|
60
|
-
description "Helps with
|
61
|
-
system_message
|
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
|
-
|
67
|
-
|
68
|
-
# As the agent streams the response
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
175
|
+
user_id: {
|
79
176
|
type: "string",
|
80
|
-
description: "
|
177
|
+
description: "ID of the user the todo should be assigned to"
|
81
178
|
},
|
82
|
-
|
83
|
-
type: "
|
84
|
-
description: "
|
179
|
+
todo_id: {
|
180
|
+
type: "string",
|
181
|
+
description: "ID of the todo to assign"
|
85
182
|
},
|
86
|
-
|
183
|
+
reason: {
|
87
184
|
type: "string",
|
88
|
-
|
89
|
-
description: "The unit of temperature"
|
185
|
+
description: "Why you decided to assign this todo to the user"
|
90
186
|
}
|
91
187
|
},
|
92
|
-
required: ["
|
93
|
-
}
|
188
|
+
required: ["user_id", "todo_id"]
|
189
|
+
})
|
94
190
|
|
95
|
-
def
|
96
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
data/lib/bristow/function.rb
CHANGED
data/lib/bristow/version.rb
CHANGED
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.
|
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-
|
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/
|
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
|