bristow 0.4.0 → 0.5.1

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: 2ef9882c1bac79c842354a58c175fd1ed1c6228140c2139246dcdc22bb693728
4
- data.tar.gz: 4f7c34334cda242b9a7dd1f34072df637aebb3d0e59ed06d2dd665219c44d210
3
+ metadata.gz: 3e0ca726d23b0633656cf1ee562ac3b501d970b84692b8cfdf91437dce4f5a48
4
+ data.tar.gz: 9016bcf5fa198bf1531ce1f0a49b7742e113f6bd7d15549d1c6af8dc61b07dd2
5
5
  SHA512:
6
- metadata.gz: 3333d030e4db031098a26c6d1ddea67ba6c23a42b58b4f90ecab5289f6565c230b8a9e10becb4c9af11c18198da66fb5b1be6174b43c1ffc4350b5b1e19ba7f0
7
- data.tar.gz: 75d286829802af2c80ba1a7638d1b27129aedf66d5e331195cffc942caeb1272eb5b5d317538aa359d2db9b627320c0e74a1c2d8e4867cf77bba7b43c12021e2
6
+ metadata.gz: edf58bbcf4cb2352863da1110c3358e501ac5163d22619235c840f4f17e020e381e5b8f1d2d2c5bab7863eb909a3c0c56f69d3a665b321c0b5ca32ce757aa3fc
7
+ data.tar.gz: 1b4504c7136d11e5d1b5d8d2b0175c9450c3ef28d3cf63cfa7990dff91a89cac5051776c081bed43a12093dd86cf3767911dd898a54f6e8b1990ac8c8342b509
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2025-05-08
4
+
5
+ - Improve publish script
6
+
7
+ ## [0.5.0] - 2025-05-08
8
+
9
+ - Add termination conditions so you can ensure an agent stop after a certain amount of time.
10
+ - Drop VCR in favor of WebMock for testing
11
+ - Fix `name` shadowing the built-in function for agent and function
12
+
3
13
  ## [0.4.0] - 2025-02-15
4
14
 
5
15
  - Update the README to make it flow a bit better.
data/README.md CHANGED
@@ -41,7 +41,7 @@ end
41
41
  # Define functions that the model can call to interact with your app
42
42
  class UserSearch < Bristow::Function
43
43
  # Name that will be provided to the AI model for function calls
44
- name "user_search"
44
+ function_name "user_search"
45
45
 
46
46
  # Description for the AI model that it can use to determine when
47
47
  # it should call this function
@@ -76,7 +76,7 @@ end
76
76
 
77
77
  # Create an agent with access to the function
78
78
  class UserQueryAssistant < Bristow::Agent
79
- name "UserQueryAssistant"
79
+ agent_name "UserQueryAssistant"
80
80
  description "Helps with user-related queries"
81
81
  system_message <<~MSG
82
82
  You are a user management assistant.
@@ -126,7 +126,7 @@ Agents can be configured similar to something like `ActiveJob` or `Sidekiq`. You
126
126
 
127
127
  ```ruby
128
128
  class Pirate < Bristow::Agent
129
- name "Pirate"
129
+ agent_name "Pirate"
130
130
  description "An agent that assists the user while always talking like a pirate."
131
131
  system_message "You are a helpful assistant that always talks like a pirate. Try to be as corny and punny as possible."
132
132
  end
@@ -141,7 +141,7 @@ end
141
141
 
142
142
  Here's an overview of all config options available when configuring an Agent:
143
143
 
144
- - `name`: The name of the agent
144
+ - `agent_name`: The name of the agent
145
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
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
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.
@@ -168,7 +168,7 @@ You can think of functions as an API for your application for the AI model. When
168
168
 
169
169
  ```ruby
170
170
  class TodoAssigner < Bristow::Function
171
- name "todo_assigner"
171
+ function_name "todo_assigner"
172
172
  description "Given a user ID and a todo ID, it will assign the todo to the user."
173
173
  parameters({
174
174
  properties: {
@@ -198,7 +198,7 @@ end
198
198
 
199
199
  Functions have 3 config options, and a `#perform` function:
200
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.
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
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
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
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.
@@ -288,7 +288,7 @@ end
288
288
 
289
289
  # You can overrided these settings on a per-agent basis like this:
290
290
  storyteller = Bristow::Agent.new(
291
- name: 'Sydney',
291
+ agent_name: 'Sydney',
292
292
  description: 'Agent for telling spy stories',
293
293
  system_message: 'Given a topic, you will tell a brief spy story',
294
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]
@@ -1,13 +1,13 @@
1
1
  require_relative '../lib/bristow'
2
2
 
3
3
  class TravelAgent < Bristow::Agent
4
- name "TravelAgent"
4
+ agent_name "TravelAgent"
5
5
  description "Agent for planning trips"
6
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
7
  end
8
8
 
9
9
  class StoryTeller < Bristow::Agent
10
- name "StoryTeller"
10
+ agent_name "StoryTeller"
11
11
  description 'An agent that tells a story given an agenda'
12
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
13
  end
@@ -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
 
@@ -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, default: -> { self.class.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.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/bristow.rb CHANGED
@@ -14,6 +14,10 @@ require_relative "bristow/agents/supervisor"
14
14
  require_relative "bristow/agency"
15
15
  require_relative "bristow/agencies/supervisor"
16
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"
17
21
 
18
22
  module Bristow
19
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.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hampton
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-16 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
@@ -125,6 +111,7 @@ files:
125
111
  - Rakefile
126
112
  - examples/basic_agency.rb
127
113
  - examples/basic_agent.rb
114
+ - examples/basic_termination.rb
128
115
  - examples/function_calls.rb
129
116
  - examples/workflow_agency.rb
130
117
  - lib/bristow.rb
@@ -138,6 +125,10 @@ files:
138
125
  - lib/bristow/functions/delegate.rb
139
126
  - lib/bristow/helpers/delegate.rb
140
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
141
132
  - lib/bristow/version.rb
142
133
  homepage: https://github.com/andrewhampton/bristow
143
134
  licenses:
@@ -163,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
154
  - !ruby/object:Gem::Version
164
155
  version: '0'
165
156
  requirements: []
166
- rubygems_version: 3.6.3
157
+ rubygems_version: 3.6.8
167
158
  specification_version: 4
168
159
  summary: A Ruby framework for creating systems of cooperative AI agents with function
169
160
  calling capabilities