bristow 0.5.1 → 1.0.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/examples/README.md +33 -0
- data/examples/basic_agency.rb +13 -0
- data/examples/basic_agent.rb +9 -0
- data/examples/basic_termination.rb +13 -0
- data/examples/function_calls.rb +10 -1
- data/examples/workflow_agency.rb +13 -0
- data/lib/bristow/agent.rb +18 -62
- data/lib/bristow/configuration.rb +55 -7
- data/lib/bristow/function.rb +2 -2
- data/lib/bristow/providers/anthropic.rb +184 -0
- data/lib/bristow/providers/base.rb +49 -0
- data/lib/bristow/providers/google.rb +29 -0
- data/lib/bristow/providers/openai.rb +90 -0
- data/lib/bristow/version.rb +1 -1
- data/lib/bristow.rb +5 -0
- metadata +20 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbaf55e16f138b6a46fe9b5795cdb38f929d000e61f928131f1660cd421bf147
|
4
|
+
data.tar.gz: 13e9d03c76093fe4193d6335fdea763a7dd31737bf838bd3ad9bc1046b63d1e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87472c67f66015f8f7193a70f3fb17acc259c4c21a8380df294fef5696a1da7da5e95db6178bba75273e68c354dd22cd7f9815fe1bdd6ef0bfa0b822aacaa34d
|
7
|
+
data.tar.gz: 62f1fd087cd3c30544098de3a1fc786e0a947da6d909de42bc9537a372b252b86ea7cc66c8f8c6bcf6683ca7fa75315faa9d8869da8b1258f7a678d3530093e2
|
data/CHANGELOG.md
CHANGED
data/examples/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Bristow Examples
|
2
|
+
|
3
|
+
This directory contains examples demonstrating various features of Bristow.
|
4
|
+
|
5
|
+
## Provider Selection
|
6
|
+
|
7
|
+
All examples support provider selection via the `BRISTOW_PROVIDER` environment variable:
|
8
|
+
|
9
|
+
### OpenAI (default)
|
10
|
+
```bash
|
11
|
+
ruby basic_agent.rb
|
12
|
+
# or explicitly
|
13
|
+
BRISTOW_PROVIDER=openai ruby basic_agent.rb
|
14
|
+
```
|
15
|
+
|
16
|
+
### Anthropic (Claude)
|
17
|
+
```bash
|
18
|
+
BRISTOW_PROVIDER=anthropic ruby basic_agent.rb
|
19
|
+
```
|
20
|
+
|
21
|
+
Make sure to set the appropriate API key:
|
22
|
+
- `OPENAI_API_KEY` for OpenAI
|
23
|
+
- `ANTHROPIC_API_KEY` for Anthropic
|
24
|
+
|
25
|
+
## Examples
|
26
|
+
|
27
|
+
- **basic_agent.rb** - Simple agent that tells spy stories
|
28
|
+
- **function_calls.rb** - Agent with function calling capabilities (weather lookup)
|
29
|
+
- **basic_agency.rb** - Multi-agent system with supervisor coordination
|
30
|
+
- **basic_termination.rb** - Agent with conversation termination conditions
|
31
|
+
- **workflow_agency.rb** - Sequential workflow between agents
|
32
|
+
|
33
|
+
All examples work identically across providers thanks to Bristow's universal function schema format.
|
data/examples/basic_agency.rb
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
require_relative '../lib/bristow'
|
2
2
|
|
3
|
+
Bristow.configure do |config|
|
4
|
+
# Set provider based on environment variable, default to OpenAI
|
5
|
+
provider = ENV['BRISTOW_PROVIDER']&.to_sym || :openai
|
6
|
+
config.default_provider = provider
|
7
|
+
|
8
|
+
case provider
|
9
|
+
when :anthropic
|
10
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
11
|
+
when :openai
|
12
|
+
config.model = 'gpt-4o-mini'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
3
16
|
class PirateTalker < Bristow::Agent
|
4
17
|
agent_name "PirateSpeaker"
|
5
18
|
description "Agent for translating input to pirate-speak"
|
data/examples/basic_agent.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
require_relative '../lib/bristow'
|
2
2
|
|
3
3
|
Bristow.configure do |config|
|
4
|
+
# Set provider based on environment variable, default to OpenAI
|
5
|
+
provider = ENV['BRISTOW_PROVIDER']&.to_sym || :openai
|
6
|
+
config.default_provider = provider
|
7
|
+
|
8
|
+
case provider
|
9
|
+
when :anthropic
|
10
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
11
|
+
when :openai
|
4
12
|
config.model = 'gpt-4o-mini'
|
13
|
+
end
|
5
14
|
end
|
6
15
|
|
7
16
|
class Sydney < Bristow::Agent
|
@@ -1,5 +1,18 @@
|
|
1
1
|
require_relative '../lib/bristow'
|
2
2
|
|
3
|
+
Bristow.configure do |config|
|
4
|
+
# Set provider based on environment variable, default to OpenAI
|
5
|
+
provider = ENV['BRISTOW_PROVIDER']&.to_sym || :openai
|
6
|
+
config.default_provider = provider
|
7
|
+
|
8
|
+
case provider
|
9
|
+
when :anthropic
|
10
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
11
|
+
when :openai
|
12
|
+
config.model = 'gpt-4o-mini'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
3
16
|
class CountAgent < Bristow::Agent
|
4
17
|
agent_name "CounterAgent"
|
5
18
|
description "Knows how to count"
|
data/examples/function_calls.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
require_relative '../lib/bristow'
|
2
2
|
|
3
3
|
Bristow.configure do |config|
|
4
|
+
# Set provider based on environment variable, default to OpenAI
|
5
|
+
provider = ENV['BRISTOW_PROVIDER']&.to_sym || :openai
|
6
|
+
config.default_provider = provider
|
7
|
+
|
8
|
+
case provider
|
9
|
+
when :anthropic
|
10
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
11
|
+
when :openai
|
4
12
|
config.model = 'gpt-4o-mini'
|
13
|
+
end
|
5
14
|
end
|
6
15
|
|
7
16
|
# Define functions that GPT can call
|
@@ -39,7 +48,7 @@ class WeatherAgent < Bristow::Agent
|
|
39
48
|
end
|
40
49
|
|
41
50
|
# Start a conversation
|
42
|
-
messages = WeatherAgent.chat("What's the weather like in London?") do |part|
|
51
|
+
messages = WeatherAgent.chat("What's the weather like in London, UK?") do |part|
|
43
52
|
print part
|
44
53
|
end
|
45
54
|
|
data/examples/workflow_agency.rb
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
require_relative '../lib/bristow'
|
2
2
|
|
3
|
+
Bristow.configure do |config|
|
4
|
+
# Set provider based on environment variable, default to OpenAI
|
5
|
+
provider = ENV['BRISTOW_PROVIDER']&.to_sym || :openai
|
6
|
+
config.default_provider = provider
|
7
|
+
|
8
|
+
case provider
|
9
|
+
when :anthropic
|
10
|
+
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
11
|
+
when :openai
|
12
|
+
config.model = 'gpt-4o-mini'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
3
16
|
class TravelAgent < Bristow::Agent
|
4
17
|
agent_name "TravelAgent"
|
5
18
|
description "Agent for planning trips"
|
data/lib/bristow/agent.rb
CHANGED
@@ -6,12 +6,13 @@ module Bristow
|
|
6
6
|
sgetter :description
|
7
7
|
sgetter :system_message
|
8
8
|
sgetter :functions, default: []
|
9
|
+
sgetter :provider, default: -> { Bristow.configuration.default_provider }
|
9
10
|
sgetter :model, default: -> { Bristow.configuration.model }
|
10
|
-
sgetter :client, default:
|
11
|
+
sgetter :client, default: nil
|
11
12
|
sgetter :logger, default: -> { Bristow.configuration.logger }
|
12
13
|
sgetter :termination, default: -> { Bristow::Terminations::MaxMessages.new(100) }
|
13
14
|
attr_reader :chat_history
|
14
|
-
|
15
|
+
|
15
16
|
|
16
17
|
|
17
18
|
def initialize(
|
@@ -19,6 +20,7 @@ module Bristow
|
|
19
20
|
description: self.class.description,
|
20
21
|
system_message: self.class.system_message,
|
21
22
|
functions: self.class.functions.dup,
|
23
|
+
provider: self.class.provider,
|
22
24
|
model: self.class.model,
|
23
25
|
client: self.class.client,
|
24
26
|
logger: self.class.logger,
|
@@ -28,8 +30,9 @@ module Bristow
|
|
28
30
|
@description = description
|
29
31
|
@system_message = system_message
|
30
32
|
@functions = functions
|
33
|
+
@provider = provider
|
31
34
|
@model = model
|
32
|
-
@client = client
|
35
|
+
@client = client || Bristow.configuration.client_for(@provider)
|
33
36
|
@logger = logger
|
34
37
|
@chat_history = []
|
35
38
|
@termination = termination
|
@@ -41,8 +44,8 @@ module Bristow
|
|
41
44
|
function.call(**arguments.transform_keys(&:to_sym))
|
42
45
|
end
|
43
46
|
|
44
|
-
def
|
45
|
-
|
47
|
+
def formatted_functions
|
48
|
+
client.format_functions(functions)
|
46
49
|
end
|
47
50
|
|
48
51
|
def self.chat(...)
|
@@ -65,38 +68,33 @@ module Bristow
|
|
65
68
|
}
|
66
69
|
|
67
70
|
if functions.any?
|
68
|
-
params
|
69
|
-
params[:function_call] = "auto"
|
71
|
+
params.merge!(formatted_functions)
|
70
72
|
end
|
71
73
|
|
72
|
-
logger.debug("Calling
|
74
|
+
logger.debug("Calling #{provider} API with params: #{params}")
|
73
75
|
response_message = if block_given?
|
74
|
-
|
76
|
+
client.stream_chat(params, &block)
|
75
77
|
else
|
76
|
-
|
77
|
-
response.dig("choices", 0, "message")
|
78
|
+
client.chat(params)
|
78
79
|
end
|
79
80
|
|
80
81
|
messages << response_message
|
81
82
|
@chat_history << response_message
|
82
83
|
|
83
84
|
# If there's no function call, we're done
|
84
|
-
break unless response_message
|
85
|
+
break unless client.is_function_call?(response_message)
|
85
86
|
|
86
87
|
# Handle the function call and add its result to the messages
|
87
88
|
result = handle_function_call(
|
88
|
-
response_message
|
89
|
-
|
89
|
+
client.function_name(response_message),
|
90
|
+
client.function_arguments(response_message)
|
90
91
|
)
|
91
92
|
|
92
|
-
yield "\n[Function Call: #{response_message
|
93
|
+
yield "\n[Function Call: #{response_message}]\n" if block_given?
|
93
94
|
yield "#{result.to_json}\n" if block_given?
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
"name" => response_message["function_call"]["name"],
|
98
|
-
"content" => result.to_json
|
99
|
-
}
|
96
|
+
|
97
|
+
messages << client.format_function_response(response_message, result)
|
100
98
|
end
|
101
99
|
|
102
100
|
messages
|
@@ -107,49 +105,7 @@ module Bristow
|
|
107
105
|
|
108
106
|
private
|
109
107
|
|
110
|
-
def handle_streaming_chat(params)
|
111
|
-
full_content = ""
|
112
|
-
function_name = nil
|
113
|
-
function_args = ""
|
114
|
-
|
115
|
-
stream_proc = proc do |chunk|
|
116
|
-
delta = chunk.dig("choices", 0, "delta")
|
117
|
-
next unless delta
|
118
|
-
|
119
|
-
if delta["function_call"]
|
120
|
-
# Building function call
|
121
|
-
if delta.dig("function_call", "name")
|
122
|
-
function_name = delta.dig("function_call", "name")
|
123
|
-
end
|
124
|
-
|
125
|
-
if delta.dig("function_call", "arguments")
|
126
|
-
function_args += delta.dig("function_call", "arguments")
|
127
|
-
end
|
128
|
-
elsif delta["content"]
|
129
|
-
# Regular content
|
130
|
-
full_content += delta["content"]
|
131
|
-
yield delta["content"]
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
params[:stream] = stream_proc
|
136
|
-
client.chat(parameters: params)
|
137
108
|
|
138
|
-
if function_name
|
139
|
-
{
|
140
|
-
"role" => "assistant",
|
141
|
-
"function_call" => {
|
142
|
-
"name" => function_name,
|
143
|
-
"arguments" => function_args
|
144
|
-
}
|
145
|
-
}
|
146
|
-
else
|
147
|
-
{
|
148
|
-
"role" => "assistant",
|
149
|
-
"content" => full_content
|
150
|
-
}
|
151
|
-
end
|
152
|
-
end
|
153
109
|
|
154
110
|
def system_message_hash
|
155
111
|
{
|
@@ -1,24 +1,72 @@
|
|
1
1
|
module Bristow
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :openai_api_key, :
|
4
|
-
|
3
|
+
attr_accessor :openai_api_key, :anthropic_api_key, :google_api_key,
|
4
|
+
:default_provider, :default_model, :logger
|
5
|
+
attr_reader :clients
|
5
6
|
|
6
7
|
def initialize
|
7
8
|
@openai_api_key = ENV['OPENAI_API_KEY']
|
8
|
-
@
|
9
|
+
@anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
10
|
+
@google_api_key = ENV['GOOGLE_API_KEY']
|
11
|
+
@default_provider = :openai
|
12
|
+
@default_model = nil # Will use provider's default
|
9
13
|
@logger = Logger.new(STDOUT)
|
10
|
-
|
14
|
+
@clients = {}
|
11
15
|
end
|
12
16
|
|
13
17
|
def openai_api_key=(key)
|
14
18
|
@openai_api_key = key
|
15
|
-
reset_client
|
19
|
+
reset_client(:openai)
|
20
|
+
end
|
21
|
+
|
22
|
+
def anthropic_api_key=(key)
|
23
|
+
@anthropic_api_key = key
|
24
|
+
reset_client(:anthropic)
|
25
|
+
end
|
26
|
+
|
27
|
+
def google_api_key=(key)
|
28
|
+
@google_api_key = key
|
29
|
+
reset_client(:google)
|
30
|
+
end
|
31
|
+
|
32
|
+
def client_for(provider)
|
33
|
+
@clients[provider] ||= build_client_for(provider)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Backward compatibility
|
37
|
+
def client
|
38
|
+
client_for(default_provider)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Backward compatibility
|
42
|
+
def model
|
43
|
+
@default_model || client_for(default_provider).default_model
|
44
|
+
end
|
45
|
+
|
46
|
+
def model=(model)
|
47
|
+
@default_model = model
|
16
48
|
end
|
17
49
|
|
18
50
|
private
|
19
51
|
|
20
|
-
def
|
21
|
-
|
52
|
+
def build_client_for(provider)
|
53
|
+
case provider
|
54
|
+
when :openai
|
55
|
+
raise ArgumentError, "OpenAI API key not configured" unless @openai_api_key
|
56
|
+
Providers::Openai.new(api_key: @openai_api_key)
|
57
|
+
when :anthropic
|
58
|
+
raise ArgumentError, "Anthropic API key not configured" unless @anthropic_api_key
|
59
|
+
Providers::Anthropic.new(api_key: @anthropic_api_key)
|
60
|
+
when :google
|
61
|
+
raise ArgumentError, "Google API key not configured" unless @google_api_key
|
62
|
+
Providers::Google.new(api_key: @google_api_key)
|
63
|
+
else
|
64
|
+
raise ArgumentError, "Unknown provider: #{provider}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def reset_client(provider)
|
69
|
+
@clients.delete(provider)
|
22
70
|
end
|
23
71
|
end
|
24
72
|
|
data/lib/bristow/function.rb
CHANGED
@@ -19,14 +19,14 @@ module Bristow
|
|
19
19
|
@parameters = parameters
|
20
20
|
end
|
21
21
|
|
22
|
-
def self.
|
22
|
+
def self.to_schema
|
23
23
|
{
|
24
24
|
name: function_name,
|
25
25
|
description: description,
|
26
26
|
parameters: parameters
|
27
27
|
}
|
28
28
|
end
|
29
|
-
delegate :
|
29
|
+
delegate :to_schema, to: :class
|
30
30
|
|
31
31
|
def self.call(...)
|
32
32
|
new.call(...)
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Bristow
|
2
|
+
module Providers
|
3
|
+
class Anthropic < Base
|
4
|
+
def initialize(api_key:)
|
5
|
+
super
|
6
|
+
@client = ::Anthropic::Client.new(api_key: api_key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def chat(params)
|
10
|
+
# Convert messages format for Anthropic
|
11
|
+
anthropic_params = convert_params(params)
|
12
|
+
|
13
|
+
response = @client.messages.create(anthropic_params)
|
14
|
+
|
15
|
+
# Convert response back to standard format
|
16
|
+
if response.content.any? { |content| content.type == "tool_use" }
|
17
|
+
# Handle tool use response
|
18
|
+
tool_use = response.content.find { |content| content.type == "tool_use" }
|
19
|
+
{
|
20
|
+
"role" => "assistant",
|
21
|
+
"function_call" => {
|
22
|
+
"name" => tool_use.name,
|
23
|
+
"arguments" => tool_use.input.to_json
|
24
|
+
}
|
25
|
+
}
|
26
|
+
else
|
27
|
+
# Handle text response
|
28
|
+
text_content = response.content.find { |content| content.type == "text" }&.text || ""
|
29
|
+
{
|
30
|
+
"role" => "assistant",
|
31
|
+
"content" => text_content
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def stream_chat(params, &block)
|
37
|
+
anthropic_params = convert_params(params)
|
38
|
+
|
39
|
+
full_content = ""
|
40
|
+
tool_use = {}
|
41
|
+
tool_use_content_json = ""
|
42
|
+
|
43
|
+
stream = @client.messages.stream_raw(anthropic_params)
|
44
|
+
stream.each do |event|
|
45
|
+
case event.type
|
46
|
+
when :content_block_delta
|
47
|
+
if event.delta.type == :text_delta
|
48
|
+
text = event.delta.text
|
49
|
+
full_content += text
|
50
|
+
yield text if block_given?
|
51
|
+
elsif event.delta.type == :input_json_delta
|
52
|
+
# Accumulate tool input JSON fragments
|
53
|
+
tool_use_content_json += event.delta.partial_json || ""
|
54
|
+
end
|
55
|
+
when :content_block_start
|
56
|
+
if event.content_block.type == :tool_use
|
57
|
+
tool_use = event.content_block
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if tool_use.to_hash.any?
|
63
|
+
{
|
64
|
+
"role" => "assistant",
|
65
|
+
content: [tool_use.to_hash.merge({input: JSON.parse(tool_use_content_json)})]
|
66
|
+
}
|
67
|
+
else
|
68
|
+
{
|
69
|
+
"role" => "assistant",
|
70
|
+
"content" => full_content
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_functions(functions)
|
76
|
+
{
|
77
|
+
tools: functions.map { |func| convert_function_to_tool(func.to_schema) }
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def is_function_call?(response)
|
82
|
+
response.dig(:content, 0, :type) == :tool_use
|
83
|
+
end
|
84
|
+
|
85
|
+
def function_name(response)
|
86
|
+
# TODO: support parallel function calls
|
87
|
+
response[:content][0][:name]
|
88
|
+
end
|
89
|
+
|
90
|
+
def function_arguments(response)
|
91
|
+
# TODO: support parallel function calls
|
92
|
+
response[:content][0][:input]
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_function_response(response, result)
|
96
|
+
{
|
97
|
+
role: :user,
|
98
|
+
content: [{
|
99
|
+
type: "tool_result",
|
100
|
+
tool_use_id: response[:content][0][:id],
|
101
|
+
content: result.to_json
|
102
|
+
}]
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def default_model
|
107
|
+
"claude-3-5-sonnet-20241022"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def convert_params(params)
|
113
|
+
# Extract system messages and regular messages
|
114
|
+
messages = params[:messages] || []
|
115
|
+
system_messages = messages.select { |msg| msg["role"] == "system" || msg[:role] == "system" }
|
116
|
+
other_messages = messages.reject { |msg| msg["role"] == "system" || msg[:role] == "system" }
|
117
|
+
|
118
|
+
anthropic_params = {
|
119
|
+
model: params[:model] || default_model,
|
120
|
+
max_tokens: params[:max_tokens] || 1024,
|
121
|
+
messages: convert_messages(other_messages)
|
122
|
+
}
|
123
|
+
|
124
|
+
# Add system message if present
|
125
|
+
if system_messages.any?
|
126
|
+
anthropic_params[:system] = system_messages.map { |msg| msg["content"] || msg[:content] }.join("\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add tools if present
|
130
|
+
if params[:tools]
|
131
|
+
anthropic_params[:tools] = params[:tools]
|
132
|
+
elsif params[:functions]
|
133
|
+
anthropic_params[:tools] = params[:functions].map { |func| convert_function_to_tool(func) }
|
134
|
+
end
|
135
|
+
|
136
|
+
anthropic_params
|
137
|
+
end
|
138
|
+
|
139
|
+
def convert_messages(messages)
|
140
|
+
messages.map do |msg|
|
141
|
+
role = msg["role"] || msg[:role]
|
142
|
+
case role
|
143
|
+
when "function"
|
144
|
+
# Convert function result to user message
|
145
|
+
{
|
146
|
+
"role" => "user",
|
147
|
+
"content" => "Function result: #{msg['content'] || msg[:content]}"
|
148
|
+
}
|
149
|
+
else
|
150
|
+
# Handle regular user/assistant messages
|
151
|
+
function_call = msg["function_call"] || msg[:function_call]
|
152
|
+
if function_call
|
153
|
+
# Convert function call to tool use format
|
154
|
+
{
|
155
|
+
"role" => "assistant",
|
156
|
+
"content" => [
|
157
|
+
{
|
158
|
+
"type" => "tool_use",
|
159
|
+
"id" => "call_#{rand(1000000)}",
|
160
|
+
"name" => function_call["name"] || function_call[:name],
|
161
|
+
"input" => JSON.parse(function_call["arguments"] || function_call[:arguments])
|
162
|
+
}
|
163
|
+
]
|
164
|
+
}
|
165
|
+
else
|
166
|
+
{
|
167
|
+
"role" => role,
|
168
|
+
"content" => msg["content"] || msg[:content]
|
169
|
+
}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def convert_function_to_tool(func_schema)
|
176
|
+
{
|
177
|
+
"name" => func_schema[:name],
|
178
|
+
"description" => func_schema[:description],
|
179
|
+
"input_schema" => func_schema[:parameters]
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Bristow
|
2
|
+
module Providers
|
3
|
+
class Base
|
4
|
+
attr_reader :api_key
|
5
|
+
|
6
|
+
def initialize(api_key:)
|
7
|
+
@api_key = api_key
|
8
|
+
raise ArgumentError, "API key is required" if api_key.nil? || api_key.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Abstract method - must be implemented by subclasses
|
12
|
+
def chat(params)
|
13
|
+
raise NotImplementedError, "Subclasses must implement #chat"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Abstract method - must be implemented by subclasses
|
17
|
+
def stream_chat(params, &block)
|
18
|
+
raise NotImplementedError, "Subclasses must implement #stream_chat"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Abstract method - must be implemented by subclasses
|
22
|
+
def format_functions(functions)
|
23
|
+
raise NotImplementedError, "Subclasses must implement #format_functions"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Abstract method - must be implemented by subclasses
|
27
|
+
def default_model
|
28
|
+
raise NotImplementedError, "Subclasses must implement #default_model"
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_function_call?(response)
|
32
|
+
raise NotImplementedError, "Subclasses must implement #is_function_call?"
|
33
|
+
end
|
34
|
+
|
35
|
+
def function_name(response)
|
36
|
+
raise NotImplementedError, "Subclasses must implement #function_name"
|
37
|
+
end
|
38
|
+
|
39
|
+
def function_arguments(response)
|
40
|
+
raise NotImplementedError, "Subclasses must implement #function_arguments"
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_function_response(response, result)
|
44
|
+
raise NotImplementedError, "Subclasses must implement #format_function_response"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Bristow
|
2
|
+
module Providers
|
3
|
+
class Google < Base
|
4
|
+
def initialize(api_key:)
|
5
|
+
super
|
6
|
+
# Will implement Google client when gem is available
|
7
|
+
@client = nil # Placeholder for Google::GenerativeAI::Client.new(api_key: api_key)
|
8
|
+
end
|
9
|
+
|
10
|
+
def chat(params)
|
11
|
+
raise NotImplementedError, "Google provider not yet implemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
def stream_chat(params, &block)
|
15
|
+
raise NotImplementedError, "Google provider not yet implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
def format_functions(functions)
|
19
|
+
# Google uses function declarations format
|
20
|
+
# Will implement when Google client is added
|
21
|
+
raise NotImplementedError, "Google provider not yet implemented"
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_model
|
25
|
+
"gemini-pro"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Bristow
|
2
|
+
module Providers
|
3
|
+
class Openai < Base
|
4
|
+
def initialize(api_key:)
|
5
|
+
super
|
6
|
+
@client = OpenAI::Client.new(access_token: api_key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def chat(params)
|
10
|
+
response = @client.chat(parameters: params)
|
11
|
+
response.dig("choices", 0, "message")
|
12
|
+
end
|
13
|
+
|
14
|
+
def stream_chat(params, &block)
|
15
|
+
full_content = ""
|
16
|
+
function_name = nil
|
17
|
+
function_args = ""
|
18
|
+
|
19
|
+
stream_proc = proc do |chunk|
|
20
|
+
delta = chunk.dig("choices", 0, "delta")
|
21
|
+
next unless delta
|
22
|
+
|
23
|
+
if delta["function_call"]
|
24
|
+
# Building function call
|
25
|
+
if delta.dig("function_call", "name")
|
26
|
+
function_name = delta.dig("function_call", "name")
|
27
|
+
end
|
28
|
+
|
29
|
+
if delta.dig("function_call", "arguments")
|
30
|
+
function_args += delta.dig("function_call", "arguments")
|
31
|
+
end
|
32
|
+
elsif delta["content"]
|
33
|
+
# Regular content
|
34
|
+
full_content += delta["content"]
|
35
|
+
yield delta["content"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
params[:stream] = stream_proc
|
40
|
+
@client.chat(parameters: params)
|
41
|
+
|
42
|
+
if function_name
|
43
|
+
{
|
44
|
+
"role" => "assistant",
|
45
|
+
"function_call" => {
|
46
|
+
"name" => function_name,
|
47
|
+
"arguments" => function_args
|
48
|
+
}
|
49
|
+
}
|
50
|
+
else
|
51
|
+
{
|
52
|
+
"role" => "assistant",
|
53
|
+
"content" => full_content
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def format_functions(functions)
|
59
|
+
{
|
60
|
+
functions: functions.map(&:to_schema),
|
61
|
+
function_call: "auto"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_model
|
66
|
+
"gpt-4o-mini"
|
67
|
+
end
|
68
|
+
|
69
|
+
def is_function_call?(response)
|
70
|
+
response["function_call"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def function_name(response)
|
74
|
+
response["function_call"]["name"]
|
75
|
+
end
|
76
|
+
|
77
|
+
def function_arguments(response)
|
78
|
+
JSON.parse(response["function_call"]["arguments"])
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_function_response(response, result)
|
82
|
+
message_hash = {
|
83
|
+
"role" => "function",
|
84
|
+
"name" => response["function_call"]["name"],
|
85
|
+
"content" => result.to_json
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/bristow/version.rb
CHANGED
data/lib/bristow.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'openai'
|
3
|
+
require 'anthropic'
|
3
4
|
require 'logger'
|
4
5
|
require 'json'
|
5
6
|
|
6
7
|
require_relative "bristow/version"
|
7
8
|
require_relative "bristow/helpers/sgetter"
|
8
9
|
require_relative "bristow/helpers/delegate"
|
10
|
+
require_relative "bristow/providers/base"
|
11
|
+
require_relative "bristow/providers/openai"
|
12
|
+
require_relative "bristow/providers/anthropic"
|
13
|
+
require_relative "bristow/providers/google"
|
9
14
|
require_relative "bristow/configuration"
|
10
15
|
require_relative "bristow/function"
|
11
16
|
require_relative "bristow/functions/delegate"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bristow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Hampton
|
@@ -23,6 +23,20 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 7.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: anthropic
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
26
40
|
- !ruby/object:Gem::Dependency
|
27
41
|
name: debug
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,6 +123,7 @@ files:
|
|
109
123
|
- LICENSE.txt
|
110
124
|
- README.md
|
111
125
|
- Rakefile
|
126
|
+
- examples/README.md
|
112
127
|
- examples/basic_agency.rb
|
113
128
|
- examples/basic_agent.rb
|
114
129
|
- examples/basic_termination.rb
|
@@ -125,6 +140,10 @@ files:
|
|
125
140
|
- lib/bristow/functions/delegate.rb
|
126
141
|
- lib/bristow/helpers/delegate.rb
|
127
142
|
- lib/bristow/helpers/sgetter.rb
|
143
|
+
- lib/bristow/providers/anthropic.rb
|
144
|
+
- lib/bristow/providers/base.rb
|
145
|
+
- lib/bristow/providers/google.rb
|
146
|
+
- lib/bristow/providers/openai.rb
|
128
147
|
- lib/bristow/termination.rb
|
129
148
|
- lib/bristow/terminations/can_not_stop_will_not_stop.rb
|
130
149
|
- lib/bristow/terminations/max_messages.rb
|