swarm-agent 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: feee17f54f7b9bfd3b044457087d2dd3c581700b89e7b86d1de5dee600321c8a
4
+ data.tar.gz: 79f565ee94e692d163d152f696b0d1ec0800efdbd6d533c43b14b60d5d710545
5
+ SHA512:
6
+ metadata.gz: eaaebb83be4304001ef0d87abe365c005ca95d28254a8c9deba510a1048f197cf8f4ee6109268f1fe72f12e8c498c8ad74e3ed190aed4f6c0e664e11955604a3
7
+ data.tar.gz: 3a24b50dae453d2a4fee8da558d644baaf069e3f51eebeaa4956eb5803bb1b25ed144b0ae617f28d95d66b74b40c7bdbb4a77b881f300b7cfdab51198449720c
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 kiyo-e
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Swarm
2
+
3
+ Swarm is a Ruby library that simplifies the use of OpenAI API for chat completion and function calling.
4
+
5
+ This project was inspired by [OpenAI's swarm repository](https://github.com/openai/swarm).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'swarm'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ $ bundle install
19
+ ```
20
+
21
+ Or install it directly:
22
+
23
+ ```bash
24
+ $ gem install swarm
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Basic Usage
30
+
31
+ ```ruby
32
+ require 'swarm'
33
+
34
+ # Create an agent
35
+ agent = Swarm::Agent.new(
36
+ name: "assistant",
37
+ model: "gpt-4o-mini",
38
+ instructions: "You are a helpful assistant.",
39
+ )
40
+
41
+ # Create a Swarm instance
42
+ swarm = Swarm::Swarm.new
43
+
44
+ # Execute chat
45
+ response = swarm.run(
46
+ agent: agent,
47
+ messages: [
48
+ { role: "user", content: "Hello!" }
49
+ ]
50
+ )
51
+
52
+ puts response.messages.last["content"]
53
+ ```
54
+
55
+ ### Using Function Calling
56
+
57
+ ```ruby
58
+ def get_weather(location:)
59
+ "The weather in #{location} is sunny"
60
+ end
61
+
62
+ agent = Swarm::Agent.new(
63
+ name: "weather_bot",
64
+ model: "gpt-4o-mini",
65
+ instructions: "I am a weather information bot.",
66
+ functions: [method(:get_weather)]
67
+ )
68
+
69
+ swarm = Swarm::Swarm.new
70
+
71
+ response = swarm.run(
72
+ agent: agent,
73
+ messages: [
74
+ { role: "user", content: "What's the weather in Tokyo?" }
75
+ ]
76
+ )
77
+ ```
78
+
79
+ ### Using Streaming Response
80
+
81
+ ```ruby
82
+ swarm.run(
83
+ agent: agent,
84
+ messages: messages,
85
+ stream: true
86
+ ) do |chunk|
87
+ print chunk.dig("delta", "content")
88
+ end
89
+ ```
90
+
91
+ ## Environment Variables
92
+
93
+ - `OPENAI_API_KEY`: Set your OpenAI API key.
94
+
95
+ ## Development
96
+
97
+ Bug reports and pull requests are welcome at https://github.com/kiyo-e/swarm.
98
+
99
+ ## License
100
+
101
+ This gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarm
4
+ class Agent
5
+ attr_accessor :name, :model, :instructions, :functions, :tool_choice
6
+
7
+ def initialize(name:, model:, instructions:, functions: [], tool_choice: 'auto')
8
+ @name = name
9
+ @model = model
10
+ @instructions = instructions
11
+ @functions = functions
12
+ @tool_choice = tool_choice
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarm
4
+ class Response
5
+ attr_accessor :messages, :agent, :context_variables
6
+
7
+ def initialize(messages:, agent:, context_variables:)
8
+ @messages = messages
9
+ @agent = agent
10
+ @context_variables = context_variables
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarm
4
+ class Result
5
+ attr_accessor :value, :agent, :context_variables
6
+
7
+ def initialize(value:, agent: nil, context_variables: {})
8
+ @value = value
9
+ @agent = agent
10
+ @context_variables = context_variables
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarm
4
+ CTX_VARS_NAME = 'context_variables'
5
+
6
+ class Swarm
7
+ attr_reader :client
8
+
9
+ def initialize(client: nil)
10
+ @client = client || OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
11
+ end
12
+
13
+ def get_chat_completion(agent:, history:, context_variables:, model_override: nil, stream: false, debug: false)
14
+ context_variables = Hash.new { |h, k| h[k] = '' }.merge(context_variables)
15
+ instructions = if agent.instructions.respond_to?(:call)
16
+ agent.instructions.call(context_variables)
17
+ else
18
+ agent.instructions
19
+ end
20
+ messages = [{ role: 'system', content: instructions }] + history
21
+ debug_print(debug, 'Getting chat completion for:', messages)
22
+
23
+ functions = agent.functions.map { |f| function_to_json(f) }
24
+
25
+ functions.each do |function|
26
+ params = function[:parameters]
27
+ params[:properties].delete(CTX_VARS_NAME)
28
+ params[:required]&.delete(CTX_VARS_NAME)
29
+ end
30
+
31
+ function_call_option = case agent.tool_choice
32
+ when 'required'
33
+ 'auto'
34
+ when 'none'
35
+ 'none'
36
+ else
37
+ 'auto'
38
+ end
39
+
40
+ create_params = {
41
+ model: model_override || agent.model,
42
+ messages: messages,
43
+ functions: functions.empty? ? nil : functions,
44
+ function_call: function_call_option,
45
+ stream: stream
46
+ }
47
+
48
+ @client.chat(parameters: create_params)
49
+ end
50
+
51
+ def handle_function_result(result, debug)
52
+ case result
53
+ when Result
54
+ result
55
+ when Agent
56
+ Result.new(value: { assistant: result.name }.to_json, agent: result)
57
+ else
58
+ begin
59
+ Result.new(value: result.to_s)
60
+ rescue StandardError => e
61
+ error_message = "Failed to cast response to string: #{result}. Ensure that agent functions return a string or Result object. Error: #{e.message}"
62
+ debug_print(debug, error_message)
63
+ raise TypeError, error_message
64
+ end
65
+ end
66
+ end
67
+
68
+ def handle_function_calls(function_calls, functions, context_variables, debug)
69
+ function_map = functions.each_with_object({}) { |f, h| h[f.name] = f }
70
+ partial_response = Response.new(messages: [], agent: nil, context_variables: {})
71
+
72
+ function_calls.each do |function_call|
73
+ name = function_call['name'].to_sym
74
+ if function_map.key?(name)
75
+ args = JSON.parse(function_call['arguments'])
76
+ debug_print(debug, "Processing function call: #{name} with arguments #{args}")
77
+
78
+ func = function_map[name]
79
+ if func.method(:call).parameters.any? { |_, param_name| param_name == CTX_VARS_NAME.to_sym }
80
+ args[CTX_VARS_NAME] = context_variables
81
+ end
82
+ raw_result = func.call(**args.transform_keys(&:to_sym))
83
+
84
+ result = handle_function_result(raw_result, debug)
85
+ partial_response.messages << {
86
+ role: 'function',
87
+ name: name,
88
+ content: result.value
89
+ }
90
+ partial_response.context_variables.merge!(result.context_variables || {})
91
+ partial_response.agent = result.agent if result.agent
92
+ else
93
+ debug_print(debug, "Function #{name} not found in function map.")
94
+ partial_response.messages << {
95
+ role: 'function',
96
+ name: name,
97
+ content: "Error: Function #{name} not found."
98
+ }
99
+ end
100
+ end
101
+
102
+ partial_response
103
+ end
104
+
105
+ def run_and_stream(agent:, messages:, context_variables: {}, model_override: nil, debug: false, max_turns: Float::INFINITY, execute_functions: true)
106
+ active_agent = agent
107
+ context_variables = Marshal.load(Marshal.dump(context_variables))
108
+ history = Marshal.load(Marshal.dump(messages))
109
+ init_len = messages.length
110
+
111
+ Enumerator.new do |y|
112
+ while history.length - init_len < max_turns
113
+ message = {
114
+ content: '',
115
+ sender: active_agent.name,
116
+ role: 'assistant',
117
+ function_call: nil
118
+ }
119
+
120
+ completion = get_chat_completion(
121
+ agent: active_agent,
122
+ history: history,
123
+ context_variables: context_variables,
124
+ model_override: model_override,
125
+ stream: true,
126
+ debug: debug
127
+ )
128
+
129
+ y << { delim: 'start' }
130
+ completion.each do |chunk|
131
+ delta = chunk.dig('choices', 0, 'delta') || {}
132
+ delta['sender'] = active_agent.name if delta['role'] == 'assistant'
133
+ y << delta
134
+ delta.delete('role')
135
+ delta.delete('sender')
136
+ merge_chunk(message, delta)
137
+ end
138
+ y << { delim: 'end' }
139
+
140
+ debug_print(debug, 'Received completion:', message)
141
+ history << message
142
+
143
+ function_call = message['function_call']
144
+ if function_call.nil? || !execute_functions
145
+ debug_print(debug, 'Ending turn.')
146
+ break
147
+ end
148
+
149
+ function_calls = [function_call]
150
+ partial_response = handle_function_calls(
151
+ function_calls, active_agent.functions, context_variables, debug
152
+ )
153
+ history.concat(partial_response.messages)
154
+ context_variables.merge!(partial_response.context_variables)
155
+ active_agent = partial_response.agent if partial_response.agent
156
+ end
157
+
158
+ y << { response: Response.new(messages: history[init_len..], agent: active_agent, context_variables: context_variables) }
159
+ end
160
+ end
161
+
162
+ def run(agent:, messages:, context_variables: {}, model_override: nil, stream: false, debug: false, max_turns: Float::INFINITY, execute_functions: true)
163
+ if stream
164
+ return run_and_stream(
165
+ agent: agent,
166
+ messages: messages,
167
+ context_variables: context_variables,
168
+ model_override: model_override,
169
+ debug: debug,
170
+ max_turns: max_turns,
171
+ execute_functions: execute_functions
172
+ )
173
+ end
174
+
175
+ active_agent = agent
176
+ context_variables = Marshal.load(Marshal.dump(context_variables))
177
+ history = Marshal.load(Marshal.dump(messages))
178
+ init_len = messages.length
179
+
180
+ while history.length - init_len < max_turns && active_agent
181
+ completion = get_chat_completion(
182
+ agent: active_agent,
183
+ history: history,
184
+ context_variables: context_variables,
185
+ model_override: model_override,
186
+ stream: false,
187
+ debug: debug
188
+ )
189
+ message = completion.dig('choices', 0, 'message')
190
+ debug_print(debug, 'Received completion:', message)
191
+ message['sender'] = active_agent.name
192
+ history << message
193
+
194
+ function_call = message['function_call']
195
+ if function_call.nil? || !execute_functions
196
+ debug_print(debug, 'Ending turn.')
197
+ break
198
+ end
199
+
200
+ function_calls = [function_call]
201
+ partial_response = handle_function_calls(
202
+ function_calls, active_agent.functions, context_variables, debug
203
+ )
204
+ history.concat(partial_response.messages)
205
+ context_variables.merge!(partial_response.context_variables)
206
+ active_agent = partial_response.agent if partial_response.agent
207
+ end
208
+
209
+ Response.new(
210
+ messages: history[init_len..],
211
+ agent: active_agent,
212
+ context_variables: context_variables
213
+ )
214
+ end
215
+
216
+ private
217
+
218
+ def merge_fields(target, source)
219
+ source.each do |key, value|
220
+ if value.is_a?(String)
221
+ target[key] = (target[key] || '') + value
222
+ elsif value.is_a?(Hash)
223
+ target[key] ||= {}
224
+ merge_fields(target[key], value)
225
+ end
226
+ end
227
+ end
228
+
229
+ def merge_chunk(final_response, delta)
230
+ delta.delete('role')
231
+ merge_fields(final_response, delta)
232
+ end
233
+
234
+ def function_to_json(func)
235
+ parameters = {}
236
+ required = []
237
+
238
+ begin
239
+ func.parameters.each do |type, name|
240
+ parameters[name.to_s] = { type: 'string' }
241
+ required << name.to_s if type == :req
242
+ end
243
+ rescue StandardError => e
244
+ raise ArgumentError, "Failed to get parameters for function #{func.name}: #{e.message}"
245
+ end
246
+
247
+ {
248
+ name: func.name.to_s,
249
+ description: func.respond_to?(:doc) ? func.doc : '',
250
+ parameters: {
251
+ type: 'object',
252
+ properties: parameters,
253
+ required: required
254
+ }
255
+ }
256
+ end
257
+
258
+ def debug_print(debug, *args)
259
+ return unless debug
260
+
261
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
262
+ message = args.map(&:to_s).join(' ')
263
+ puts "\e[97m[\e[90m#{timestamp}\e[97m]\e[90m #{message}\e[0m"
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarm
4
+ VERSION = "0.1.0"
5
+ end
data/lib/swarm.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'openai'
5
+ require 'swarm/version'
6
+ require 'swarm/agent'
7
+ require 'swarm/response'
8
+ require 'swarm/result'
9
+ require 'swarm/swarm'
10
+
11
+ module Swarm
12
+ class Error < StandardError; end
13
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: swarm-agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - kiyo-e
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-openai
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: Swarm is a Ruby library that provides a simple interface for managing
84
+ OpenAI chat completions with function calling support. It includes features like
85
+ streaming responses, context management, and function execution.
86
+ email:
87
+ - ''
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - LICENSE.txt
93
+ - README.md
94
+ - lib/swarm.rb
95
+ - lib/swarm/agent.rb
96
+ - lib/swarm/response.rb
97
+ - lib/swarm/result.rb
98
+ - lib/swarm/swarm.rb
99
+ - lib/swarm/version.rb
100
+ homepage: https://github.com/kiyo-e/swarm-agent
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ homepage_uri: https://github.com/kiyo-e/swarm-agent
105
+ changelog_uri: https://github.com/kiyo-e/swarm-agent/blob/main/CHANGELOG.md
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.1.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.5.20
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: A Ruby library for managing OpenAI chat completions with function calling
125
+ support
126
+ test_files: []