ruby-openai-swarm 0.1.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 +7 -0
- data/.github/workflows/rspec.yml +27 -0
- data/.gitignore +32 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +56 -0
- data/README.md +141 -0
- data/Rakefile +1 -0
- data/assets/logo-swarm.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +14 -0
- data/examples/basic/README.md +0 -0
- data/examples/basic/agent_handoff.rb +41 -0
- data/examples/basic/bare_minimum.rb +27 -0
- data/examples/basic/context_variables.rb +67 -0
- data/examples/basic/function_calling.rb +32 -0
- data/examples/basic/simple_loop_no_helpers.rb +38 -0
- data/examples/triage_agent/README.md +34 -0
- data/examples/triage_agent/agents.rb +84 -0
- data/examples/triage_agent/main.rb +3 -0
- data/examples/weather_agent/README.md +0 -0
- data/examples/weather_agent/agents.rb +59 -0
- data/examples/weather_agent/run.rb +0 -0
- data/lib/ruby-openai-swarm/agent.rb +21 -0
- data/lib/ruby-openai-swarm/core.rb +269 -0
- data/lib/ruby-openai-swarm/function_descriptor.rb +10 -0
- data/lib/ruby-openai-swarm/repl.rb +90 -0
- data/lib/ruby-openai-swarm/response.rb +11 -0
- data/lib/ruby-openai-swarm/result.rb +11 -0
- data/lib/ruby-openai-swarm/util.rb +78 -0
- data/lib/ruby-openai-swarm/version.rb +4 -0
- data/lib/ruby-openai-swarm.rb +18 -0
- data/ruby-openai-swarm.gemspec +28 -0
- metadata +135 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
OpenAI.configure do |config|
|
|
3
|
+
config.access_token = ENV['OPEN_ROUTER_ACCESS_TOKEN']
|
|
4
|
+
config.uri_base = "https://openrouter.ai/api/v1"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
client = OpenAISwarm.new
|
|
8
|
+
|
|
9
|
+
# Client chat parameters: {:model=>"gpt-4", :messages=>[{:role=>"system", :content=>"You are a helpful agent."}, {"role"=>"user", "content"=>"Do I need an umbrella today? I'm in chicago."}, {"role"=>"assistant", "content"=>nil, "refusal"=>nil, "tool_calls"=>[{"index"=>0, "id"=>"call_spvHva4SFuDfTUk57EhuhArl", "type"=>"function", "function"=>{"name"=>"get_weather", "arguments"=>"{\n \"location\": \"chicago\"\n}"}}], :sender=>"Weather Agent"}, {:role=>"tool", :tool_call_id=>"call_spvHva4SFuDfTUk57EhuhArl", :tool_name=>"get_weather", :content=>"{\"location\":{},\"temperature\":\"65\",\"time\":\"now\"}"}], :tools=>[{:type=>"function", :function=>{:name=>"send_email", :description=>"", :parameters=>{:type=>"object", :properties=>{:recipient=>{:type=>"string"}, :subject=>{:type=>"string"}, :body=>{:type=>"string"}}, :required=>["recipient", "subject", "body"]}}}, {:type=>"function", :function=>{:name=>"get_weather", :description=>"Get the current weather in a given location. Location MUST be a city.", :parameters=>{:type=>"object", :properties=>{:location=>{:type=>"string"}, :time=>{:type=>"string"}}, :required=>["location"]}}}], :stream=>false, :parallel_tool_calls=>true}
|
|
10
|
+
def get_weather(location, time= Time.now)
|
|
11
|
+
{ location: location, temperature: "65", time: time }.to_json
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def send_email(recipient, subject, body)
|
|
15
|
+
puts "Sending email..."
|
|
16
|
+
puts "To: #{recipient}"
|
|
17
|
+
puts "Subject: #{subject}"
|
|
18
|
+
puts "Body: #{body}"
|
|
19
|
+
puts "Sent!"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
function_instance_send_email = OpenAISwarm::FunctionDescriptor.new(
|
|
23
|
+
target_method: :send_email
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
function_instance_get_weather = OpenAISwarm::FunctionDescriptor.new(
|
|
27
|
+
target_method: :get_weather,
|
|
28
|
+
description: 'Get the current weather in a given location. Location MUST be a city.'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
weather_agent = OpenAISwarm::Agent.new(
|
|
32
|
+
name: "Weather Agent",
|
|
33
|
+
instructions: "You are a helpful agent.",
|
|
34
|
+
model: "gpt-4o-mini",
|
|
35
|
+
functions: [
|
|
36
|
+
function_instance_send_email,
|
|
37
|
+
function_instance_get_weather
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
msg1 = "Do I need an umbrella today? I'm in chicago."
|
|
42
|
+
# model: "gpt-4",
|
|
43
|
+
# return: The current temperature in Chicago is 65 degrees. It doesn't look like you'll need an umbrella today!
|
|
44
|
+
msg2 = "Tell me the weather in London."
|
|
45
|
+
# return: The current temperature in London is 65°F.
|
|
46
|
+
response = client.run(
|
|
47
|
+
messages: [{"role" => "user", "content" => msg2}],
|
|
48
|
+
agent: weather_agent,
|
|
49
|
+
debug: true,
|
|
50
|
+
)
|
|
51
|
+
# print(response.messages[-1]["content"])
|
|
52
|
+
|
|
53
|
+
response = client.run(
|
|
54
|
+
messages: [{"role" => "user", "content" => "What is the time right now?",}],
|
|
55
|
+
agent: weather_agent,
|
|
56
|
+
debug: true,
|
|
57
|
+
)
|
|
58
|
+
# p response.messages[-1]["content"]
|
|
59
|
+
# return: I'm sorry for the confusion, but as an AI, I don't have the ability to provide real-time information such as the current time. Please check the time on your device.
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module OpenAISwarm
|
|
2
|
+
class Agent
|
|
3
|
+
attr_accessor :name, :model, :instructions, :functions, :tool_choice, :parallel_tool_calls
|
|
4
|
+
|
|
5
|
+
def initialize(
|
|
6
|
+
name: "Agent",
|
|
7
|
+
model: "gpt-4",
|
|
8
|
+
instructions: "You are a helpful agent.",
|
|
9
|
+
functions: [],
|
|
10
|
+
tool_choice: nil,
|
|
11
|
+
parallel_tool_calls: true
|
|
12
|
+
)
|
|
13
|
+
@name = name
|
|
14
|
+
@model = model
|
|
15
|
+
@instructions = instructions
|
|
16
|
+
@functions = functions
|
|
17
|
+
@tool_choice = tool_choice
|
|
18
|
+
@parallel_tool_calls = parallel_tool_calls
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
require 'ruby/openai'
|
|
2
|
+
begin
|
|
3
|
+
require 'pry'
|
|
4
|
+
rescue LoadError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module OpenAISwarm
|
|
8
|
+
class Core
|
|
9
|
+
include Util
|
|
10
|
+
CTX_VARS_NAME = 'context_variables'
|
|
11
|
+
|
|
12
|
+
def initialize(client = nil)
|
|
13
|
+
@client = client || OpenAI::Client.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_chat_completion(agent, history, context_variables, model_override, stream, debug)
|
|
17
|
+
context_variables = context_variables.dup
|
|
18
|
+
instructions = agent.instructions.respond_to?(:call) ? agent.instructions.call(context_variables) : agent.instructions
|
|
19
|
+
messages = [{ role: 'system', content: instructions }] + history
|
|
20
|
+
Util.debug_print(debug, "Getting chat completion for...:", messages)
|
|
21
|
+
|
|
22
|
+
tools = agent.functions.map { |f| Util.function_to_json(f) }
|
|
23
|
+
# hide context_variables from model
|
|
24
|
+
tools.each do |tool|
|
|
25
|
+
params = tool[:function][:parameters]
|
|
26
|
+
params[:properties].delete(CTX_VARS_NAME.to_sym)
|
|
27
|
+
params[:required]&.delete(CTX_VARS_NAME.to_sym)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
create_params = {
|
|
31
|
+
model: model_override || agent.model,
|
|
32
|
+
messages: messages,
|
|
33
|
+
tools: tools.empty? ? nil : tools,
|
|
34
|
+
stream: stream
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# TODO: https://platform.openai.com/docs/guides/function-calling/how-do-functions-differ-from-tools
|
|
38
|
+
# create_params[:functions] = tools unless tools.empty?
|
|
39
|
+
# create_params[:function_call] = agent.tool_choice if agent.tool_choice
|
|
40
|
+
|
|
41
|
+
create_params[:tool_choice] = agent.tool_choice if agent.tool_choice
|
|
42
|
+
create_params[:parallel_tool_calls] = agent.parallel_tool_calls if tools.any?
|
|
43
|
+
|
|
44
|
+
Util.debug_print(debug, "Client chat parameters:", create_params)
|
|
45
|
+
response = @client.chat(parameters: create_params)
|
|
46
|
+
Util.debug_print(debug, "API Response:", response)
|
|
47
|
+
response
|
|
48
|
+
rescue OpenAI::Error => e
|
|
49
|
+
Util.debug_print(true, "OpenAI API Error:", e.message)
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_function_result(result, debug)
|
|
54
|
+
case result
|
|
55
|
+
when Result
|
|
56
|
+
result
|
|
57
|
+
when Agent
|
|
58
|
+
Result.new(
|
|
59
|
+
value: JSON.generate({ assistant: result.name }),
|
|
60
|
+
agent: result
|
|
61
|
+
)
|
|
62
|
+
else
|
|
63
|
+
begin
|
|
64
|
+
Result.new(value: result.to_s)
|
|
65
|
+
rescue => e
|
|
66
|
+
error_message = "Failed to cast response to string: #{result}. Make sure agent functions return a string or Result object. Error: #{e}"
|
|
67
|
+
Util.debug_print(debug, error_message)
|
|
68
|
+
raise TypeError, error_message
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_tool_calls(tool_calls, active_agent, context_variables, debug)
|
|
74
|
+
functions = active_agent.functions
|
|
75
|
+
|
|
76
|
+
function_map = functions.map do |f|
|
|
77
|
+
if f.is_a?(OpenAISwarm::FunctionDescriptor)
|
|
78
|
+
[f.target_method.name, f.target_method]
|
|
79
|
+
else
|
|
80
|
+
[f.name, f]
|
|
81
|
+
end
|
|
82
|
+
end.to_h.transform_keys(&:to_s)
|
|
83
|
+
|
|
84
|
+
partial_response = Response.new(
|
|
85
|
+
messages: [],
|
|
86
|
+
agent: nil,
|
|
87
|
+
context_variables: {}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
tool_calls.each do |tool_call|
|
|
91
|
+
name = tool_call.dig('function', 'name')
|
|
92
|
+
unless function_map.key?(name)
|
|
93
|
+
Util.debug_print(debug, "Tool #{name} not found in function map.")
|
|
94
|
+
partial_response.messages << {
|
|
95
|
+
role: 'tool',
|
|
96
|
+
tool_call_id: tool_call.id,
|
|
97
|
+
tool_name: name,
|
|
98
|
+
content: "Error: Tool #{name} not found."
|
|
99
|
+
}
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
args = JSON.parse(tool_call.dig('function', 'arguments') || '{}')
|
|
104
|
+
Util.debug_print(debug, "Processing tool call: #{name} with arguments #{args}")
|
|
105
|
+
|
|
106
|
+
func = function_map[name]
|
|
107
|
+
# pass context_variables to agent functions
|
|
108
|
+
args[CTX_VARS_NAME] = context_variables if func.parameters.map(&:last).include?(CTX_VARS_NAME.to_sym)
|
|
109
|
+
is_parameters = func.parameters.any?
|
|
110
|
+
arguments = args.transform_keys(&:to_sym)
|
|
111
|
+
|
|
112
|
+
raw_result = is_parameters ? func.call(**arguments) : func.call
|
|
113
|
+
result = handle_function_result(raw_result, debug)
|
|
114
|
+
|
|
115
|
+
partial_response.messages << {
|
|
116
|
+
role: 'tool',
|
|
117
|
+
tool_call_id: tool_call['id'],
|
|
118
|
+
tool_name: name,
|
|
119
|
+
content: result.value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
partial_response.context_variables.merge!(result.context_variables)
|
|
123
|
+
partial_response.agent = result.agent if result.agent
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
partial_response
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run(agent:, messages:, context_variables: {}, model_override: nil, stream: false, debug: false, max_turns: Float::INFINITY, execute_tools: true)
|
|
130
|
+
if stream
|
|
131
|
+
return run_and_stream(
|
|
132
|
+
agent: agent,
|
|
133
|
+
messages: messages,
|
|
134
|
+
context_variables: context_variables,
|
|
135
|
+
model_override: model_override,
|
|
136
|
+
debug: debug,
|
|
137
|
+
max_turns: max_turns,
|
|
138
|
+
execute_tools: execute_tools
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
active_agent = agent
|
|
143
|
+
context_variables = context_variables.dup
|
|
144
|
+
history = messages.dup
|
|
145
|
+
init_len = messages.length
|
|
146
|
+
|
|
147
|
+
while history.length - init_len < max_turns && active_agent
|
|
148
|
+
completion = get_chat_completion(
|
|
149
|
+
active_agent,
|
|
150
|
+
history,
|
|
151
|
+
context_variables,
|
|
152
|
+
model_override,
|
|
153
|
+
stream,
|
|
154
|
+
debug
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
message = completion.dig('choices', 0, 'message')
|
|
158
|
+
Util.debug_print(debug, "Received completion:", message)
|
|
159
|
+
|
|
160
|
+
message['sender'] = active_agent.name
|
|
161
|
+
history << message
|
|
162
|
+
|
|
163
|
+
break if !message['tool_calls'] || !execute_tools
|
|
164
|
+
|
|
165
|
+
partial_response = handle_tool_calls(
|
|
166
|
+
message['tool_calls'],
|
|
167
|
+
active_agent,
|
|
168
|
+
context_variables,
|
|
169
|
+
debug
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
history.concat(partial_response.messages)
|
|
173
|
+
context_variables.merge!(partial_response.context_variables)
|
|
174
|
+
active_agent = partial_response.agent if partial_response.agent
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
Response.new(
|
|
178
|
+
messages: history[init_len..],
|
|
179
|
+
agent: active_agent,
|
|
180
|
+
context_variables: context_variables
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def run_and_stream(agent:, messages:, context_variables: {}, model_override: nil, debug: false, max_turns: Float::INFINITY, execute_tools: true)
|
|
187
|
+
active_agent = agent
|
|
188
|
+
context_variables = context_variables.dup
|
|
189
|
+
history = messages.dup
|
|
190
|
+
init_len = messages.length
|
|
191
|
+
|
|
192
|
+
while history.length - init_len < max_turns && active_agent
|
|
193
|
+
message = {
|
|
194
|
+
content: "",
|
|
195
|
+
sender: agent.name,
|
|
196
|
+
role: "assistant",
|
|
197
|
+
function_call: nil,
|
|
198
|
+
tool_calls: Hash.new do |h, k|
|
|
199
|
+
h[k] = {
|
|
200
|
+
function: { arguments: "", name: "" },
|
|
201
|
+
id: "",
|
|
202
|
+
type: ""
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
completion = get_chat_completion(
|
|
208
|
+
active_agent,
|
|
209
|
+
history,
|
|
210
|
+
context_variables,
|
|
211
|
+
model_override,
|
|
212
|
+
true, # stream
|
|
213
|
+
debug
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
yield({ delim: "start" })
|
|
217
|
+
completion.each do |chunk|
|
|
218
|
+
delta = JSON.parse(chunk.choices[0].delta.to_json, symbolize_names: true)
|
|
219
|
+
if delta[:role] == "assistant"
|
|
220
|
+
delta[:sender] = active_agent.name
|
|
221
|
+
end
|
|
222
|
+
yield delta
|
|
223
|
+
delta.delete(:role)
|
|
224
|
+
delta.delete(:sender)
|
|
225
|
+
Util.merge_chunk(message, delta)
|
|
226
|
+
end
|
|
227
|
+
yield({ delim: "end" })
|
|
228
|
+
|
|
229
|
+
message[:tool_calls] = message[:tool_calls].values
|
|
230
|
+
message[:tool_calls] = nil if message[:tool_calls].empty?
|
|
231
|
+
Util.debug_print(debug, "Received completion:", message)
|
|
232
|
+
history << message
|
|
233
|
+
|
|
234
|
+
break if !message[:tool_calls] || !execute_tools
|
|
235
|
+
|
|
236
|
+
# convert tool_calls to objects
|
|
237
|
+
tool_calls = message[:tool_calls].map do |tool_call|
|
|
238
|
+
OpenStruct.new(
|
|
239
|
+
id: tool_call[:id],
|
|
240
|
+
function: OpenStruct.new(
|
|
241
|
+
arguments: tool_call[:function][:arguments],
|
|
242
|
+
name: tool_call[:function][:name]
|
|
243
|
+
),
|
|
244
|
+
type: tool_call[:type]
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
partial_response = handle_tool_calls(
|
|
249
|
+
tool_calls,
|
|
250
|
+
active_agent.functions, # TODO: will check
|
|
251
|
+
context_variables,
|
|
252
|
+
debug
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
history.concat(partial_response.messages)
|
|
256
|
+
context_variables.merge!(partial_response.context_variables)
|
|
257
|
+
active_agent = partial_response.agent if partial_response.agent
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
yield({
|
|
261
|
+
response: Response.new(
|
|
262
|
+
messages: history[init_len..],
|
|
263
|
+
agent: active_agent,
|
|
264
|
+
context_variables: context_variables
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module OpenAISwarm
|
|
2
|
+
class FunctionDescriptor
|
|
3
|
+
attr_reader :target_method, :description
|
|
4
|
+
|
|
5
|
+
def initialize(target_method:, description: '')
|
|
6
|
+
@target_method = target_method.is_a?(Method) ? target_method : method(target_method)
|
|
7
|
+
@description = description
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module OpenAISwarm
|
|
2
|
+
class Repl
|
|
3
|
+
class << self
|
|
4
|
+
def process_and_print_streaming_response(response)
|
|
5
|
+
content = ""
|
|
6
|
+
last_sender = ""
|
|
7
|
+
response.each do |chunk|
|
|
8
|
+
last_sender = chunk['sender'] if chunk.key?('sender')
|
|
9
|
+
|
|
10
|
+
if chunk.key?("content") && !chunk["content"].nil?
|
|
11
|
+
if content.empty? && !last_sender.empty?
|
|
12
|
+
print "\033[94m#{last_sender}:\033[0m "
|
|
13
|
+
last_sender = ""
|
|
14
|
+
end
|
|
15
|
+
print chunk["content"]
|
|
16
|
+
content += chunk["content"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if chunk.key?("tool_calls") && !chunk["tool_calls"].nil?
|
|
20
|
+
chunk["tool_calls"].each do |tool_call|
|
|
21
|
+
f = tool_call["function"]
|
|
22
|
+
name = f["name"]
|
|
23
|
+
next if name.empty?
|
|
24
|
+
print "\033[94m#{last_sender}:\033[95m#{name}\033[0m()"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if chunk.key?("delim") && chunk["delim"] == "end" && !content.empty?
|
|
29
|
+
puts
|
|
30
|
+
content = ""
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return chunk["response"] if chunk.key?("response")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pretty_print_messages(messages)
|
|
38
|
+
messages.each do |message|
|
|
39
|
+
next unless message["role"] == "assistant"
|
|
40
|
+
|
|
41
|
+
print "\033[94m#{message[:sender]}\033[0m: "
|
|
42
|
+
|
|
43
|
+
puts message["content"] if message["content"]
|
|
44
|
+
|
|
45
|
+
tool_calls = message.fetch("tool_calls", [])
|
|
46
|
+
puts if tool_calls.length > 1
|
|
47
|
+
tool_calls.each do |tool_call|
|
|
48
|
+
func = tool_call["function"]
|
|
49
|
+
name = func["name"]
|
|
50
|
+
args = JSON.parse(func["arguments"] || "{}").map { |k, v| "#{k}=#{v}" }.join(", ")
|
|
51
|
+
puts "\e[95m#{name}\e[0m(#{args})"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def run_demo_loop(starting_agent, context_variables: {}, stream: false, debug: false)
|
|
57
|
+
client = OpenAISwarm.new
|
|
58
|
+
puts "Starting Swarm CLI 🐝"
|
|
59
|
+
|
|
60
|
+
messages = []
|
|
61
|
+
agent = starting_agent
|
|
62
|
+
|
|
63
|
+
loop do
|
|
64
|
+
print "\033[90mUser\033[0m: "
|
|
65
|
+
user_input = gets.chomp
|
|
66
|
+
break if user_input.downcase == "exit"
|
|
67
|
+
|
|
68
|
+
messages << { "role" => "user", "content" => user_input }
|
|
69
|
+
|
|
70
|
+
response = client.run(
|
|
71
|
+
agent: agent,
|
|
72
|
+
messages: messages,
|
|
73
|
+
context_variables: context_variables,
|
|
74
|
+
stream: stream,
|
|
75
|
+
debug: debug
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if stream
|
|
79
|
+
response = process_and_print_streaming_response(response)
|
|
80
|
+
else
|
|
81
|
+
pretty_print_messages(response.messages)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
messages.concat(response.messages)
|
|
85
|
+
agent = response.agent
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module OpenAISwarm
|
|
2
|
+
module Util
|
|
3
|
+
def self.debug_print(debug, *args)
|
|
4
|
+
return unless debug
|
|
5
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
|
6
|
+
message = args.map(&:to_s).join(' ')
|
|
7
|
+
puts "\e[97m[\e[90m#{timestamp}\e[97m]\e[90m #{message}\e[0m"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.merge_fields(target, source)
|
|
11
|
+
source.each do |key, value|
|
|
12
|
+
if value.is_a?(String)
|
|
13
|
+
target[key] = target[key].to_s + value
|
|
14
|
+
elsif value && value.is_a?(Hash)
|
|
15
|
+
target[key] ||= {}
|
|
16
|
+
merge_fields(target[key], value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.merge_chunk(final_response, delta)
|
|
22
|
+
delta.delete(:role)
|
|
23
|
+
merge_fields(final_response, delta)
|
|
24
|
+
|
|
25
|
+
if delta[:tool_calls]&.any?
|
|
26
|
+
index = delta[:tool_calls][0].delete(:index)
|
|
27
|
+
merge_fields(final_response[:tool_calls][index], delta[:tool_calls][0])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.function_to_json(func_instance)
|
|
32
|
+
is_target_method = func_instance.respond_to?(:target_method) || func_instance.is_a?(OpenAISwarm::FunctionDescriptor)
|
|
33
|
+
func = is_target_method ? func_instance.target_method : func_instance
|
|
34
|
+
|
|
35
|
+
function_name = func.name
|
|
36
|
+
function_parameters = func.parameters
|
|
37
|
+
|
|
38
|
+
type_map = {
|
|
39
|
+
String => "string",
|
|
40
|
+
Integer => "integer",
|
|
41
|
+
Float => "number",
|
|
42
|
+
TrueClass => "boolean",
|
|
43
|
+
FalseClass => "boolean",
|
|
44
|
+
Array => "array",
|
|
45
|
+
Hash => "object",
|
|
46
|
+
NilClass => "null"
|
|
47
|
+
}
|
|
48
|
+
parameters = {}
|
|
49
|
+
|
|
50
|
+
function_parameters.each do |type, param_name|
|
|
51
|
+
param_type = type_map[param_name.class] || "string"
|
|
52
|
+
if param_name.to_s == 'context_variables' && type == :opt #type == :keyreq
|
|
53
|
+
param_type = 'object'
|
|
54
|
+
end
|
|
55
|
+
parameters[param_name] = { type: param_type }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
required = function_parameters
|
|
59
|
+
.select { |type, _| [:req, :keyreq].include?(type) }
|
|
60
|
+
.map { |_, name| name.to_s }
|
|
61
|
+
|
|
62
|
+
description = func_instance.respond_to?(:description) ? func_instance&.description : nil
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
type: "function",
|
|
66
|
+
function: {
|
|
67
|
+
name: function_name,
|
|
68
|
+
description: description || '',
|
|
69
|
+
parameters: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: parameters,
|
|
72
|
+
required: required
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'ruby-openai-swarm/version'
|
|
2
|
+
require 'ruby-openai-swarm/agent'
|
|
3
|
+
require 'ruby-openai-swarm/response'
|
|
4
|
+
require 'ruby-openai-swarm/result'
|
|
5
|
+
require 'ruby-openai-swarm/util'
|
|
6
|
+
require 'ruby-openai-swarm/core'
|
|
7
|
+
require 'ruby-openai-swarm/function_descriptor'
|
|
8
|
+
require 'ruby-openai-swarm/repl'
|
|
9
|
+
|
|
10
|
+
module OpenAISwarm
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def new(client = nil)
|
|
15
|
+
Core.new(client)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative 'lib/ruby-openai-swarm/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "ruby-openai-swarm"
|
|
5
|
+
spec.version = OpenAISwarm::VERSION
|
|
6
|
+
spec.authors = ["Grayson"]
|
|
7
|
+
spec.email = ["cgg5207@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = " A Ruby implementation of OpenAI function calling swarm"
|
|
10
|
+
spec.description = "Allows for creating swarms of AI agents that can call functions and interact with each other"
|
|
11
|
+
spec.homepage = "https://github.com/grayson/ruby-openai-swarm"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
spec.add_dependency "ruby-openai", "~> 7.3"
|
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
26
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
27
|
+
spec.add_development_dependency "pry"
|
|
28
|
+
end
|