ruby-openai-swarm 0.2.8 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb75077c1cd68a61448103423fec5401c54963c3341b9c7a11aef564584375d7
4
- data.tar.gz: 6950967689f250591671095dd296a0e266d3e218b41e618811a3a0c8c8c48ee2
3
+ metadata.gz: 161226570a77888c84129d825b862ab7868f1d9f54b233eee7ab13950427678e
4
+ data.tar.gz: e976c2a6bb9dae417b53fdc8f3f8216d3346a295b7e18838d89d24faff5612ce
5
5
  SHA512:
6
- metadata.gz: 0caeb42cc055869a3b9bd830e3d52cb63add1a07af767cd1e42abe8b5fb7d98565327af96a1bd36ff72616d775f3be72066eeb9298da37762afe1461ff0cd345
7
- data.tar.gz: 0d515f09b04a1229e08ee1957e5958805a512f0fd9a7bb79ddf43c0962d868b9415f27714802468be1450d6d55130df7295f4ee3ed791c6c0230e09d2fa149ac
6
+ metadata.gz: 65cb09238a9e1ee5fb57d2cd9e73b56d0259bafc3dc19290e449a5db75fefce0d0cf92e65a946f525c5e6571db70e813edaa11a26c436d2a3c28c3baa88bff9f
7
+ data.tar.gz: d90997ebfbbf64a4d9a815510db0653e07f73f2c7b53e5d5837bcfc760589ecabeca2aba87a799b92f67c1c7c82baa3cd64c44445390e6896bae16d8a5aa67e9
data/.gitignore CHANGED
@@ -30,3 +30,4 @@ build-iPhoneSimulator/
30
30
 
31
31
  # rspec failure tracking
32
32
  .rspec_status
33
+ log/openai_swarm.log
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-openai-swarm (0.2.8)
4
+ ruby-openai-swarm (0.3.0)
5
5
  ruby-openai (~> 7.3)
6
6
 
7
7
  GEM
@@ -65,4 +65,4 @@ params:
65
65
  GUIDE_EXAMPLES
66
66
  puts guide_examples
67
67
 
68
- OpenAISwarm::Repl.run_demo_loop(triage_agent, context_variables: context_variables, debug: env_debug)
68
+ OpenAISwarm::Repl.run_demo_loop(triage_agent, context_variables: context_variables, stream: true, debug: env_debug)
@@ -46,6 +46,7 @@ Details:
46
46
  Example: “What’s the weather in NYC?”
47
47
  Action: Calls get_weather with location “New York City”.
48
48
  Response: Only provides weather details.
49
+
49
50
  2. Multiple Function Calls
50
51
  Example: “Tell me the weather in New York and the latest news headlines.”
51
52
  Action: Calls get_weather for weather and get_news for news.
@@ -58,4 +59,19 @@ GUIDE_EXAMPLES
58
59
 
59
60
  puts guide_examples
60
61
 
61
- OpenAISwarm::Repl.run_demo_loop(agent, stream: true, debug: env_debug)
62
+ # OpenAISwarm::Repl.run_demo_loop(agent, stream: true, debug: env_debug)
63
+
64
+ OpenAISwarm.new.run_and_stream(
65
+ agent: agent,
66
+ debug: true,
67
+ messages: [{"role" => "user", "content" => "Tell me the weather in New York and the latest news headlines."}]
68
+ ) do |chunk|
69
+ if chunk.key?("content") && !chunk["content"].nil?
70
+ puts ">>>#{chunk}"
71
+ end
72
+
73
+ if chunk.key?('response')
74
+ binding.pry
75
+ # log_llm_request(chunk['response'])
76
+ end
77
+ end
@@ -3,11 +3,17 @@ module OpenAISwarm
3
3
  attr_accessor :name, :model, :instructions,
4
4
  :functions, :tool_choice,
5
5
  :parallel_tool_calls,
6
+ :strategy,
7
+ :noisy_tool_calls,
8
+ :temperature,
6
9
  :resource
7
10
  # These attributes can be read and written externally. They include:
8
11
  # - name: The name of the agent.
9
12
  # - model: The model used, e.g., "gpt-4".
10
13
  # - resource: Additional custom parameters or data that the agent might need.
14
+ # - noisy_tool_calls: is an array that contains the names of tool calls that should be excluded because they are considered "noise".
15
+ # These tool calls generate irrelevant or unnecessary messages that the agent should not send to OpenAI.
16
+ # When filtering messages, any message that includes these tool calls will be removed from the list, preventing them from being sent to OpenAI.
11
17
 
12
18
  def initialize(
13
19
  name: "Agent",
@@ -15,16 +21,22 @@ module OpenAISwarm
15
21
  instructions: "You are a helpful agent.",
16
22
  functions: [],
17
23
  tool_choice: nil,
24
+ temperature: nil,
18
25
  parallel_tool_calls: true,
19
- resource: nil
26
+ resource: nil,
27
+ noisy_tool_calls: [],
28
+ strategy: {}
20
29
  )
21
30
  @name = name
22
31
  @model = model
23
32
  @instructions = instructions
24
33
  @functions = functions
25
34
  @tool_choice = tool_choice
35
+ @temperature = temperature
26
36
  @parallel_tool_calls = parallel_tool_calls
27
37
  @resource = resource
38
+ @noisy_tool_calls = noisy_tool_calls
39
+ @strategy = AgentStrategyOptions.new(strategy)
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,22 @@
1
+ module OpenAISwarm
2
+ class AgentChangeTracker
3
+ attr_reader :current_agent, :previous_agent
4
+
5
+ def initialize(agent)
6
+ update(agent)
7
+ end
8
+
9
+ def update(agent)
10
+ @previous_agent = @current_agent
11
+ @current_agent = agent
12
+ end
13
+
14
+ def agent_changed?
15
+ previous_agent&.name != current_agent&.name
16
+ end
17
+
18
+ def switch_agent_reset_message?
19
+ agent_changed? && current_agent.strategy.switch_agent_reset_message
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module OpenAISwarm
2
+ class AgentStrategyOptions
3
+ attr_accessor :switch_agent_reset_message
4
+
5
+ def initialize(strategy = {})
6
+ @switch_agent_reset_message = strategy[:switch_agent_reset_message] || false
7
+ end
8
+ end
9
+ end
@@ -14,7 +14,8 @@ module OpenAISwarm
14
14
  @logger = OpenAISwarm::Logger.instance.logger
15
15
  end
16
16
 
17
- def get_chat_completion(agent, history, context_variables, model_override, stream, debug)
17
+ def get_chat_completion(agent_tracker, history, context_variables, model_override, stream, debug)
18
+ agent = agent_tracker.current_agent
18
19
  context_variables = context_variables.dup
19
20
  instructions = agent.instructions.respond_to?(:call) ? agent.instructions.call(context_variables) : agent.instructions
20
21
  messages = [{ role: 'system', content: instructions }] + history
@@ -29,9 +30,11 @@ module OpenAISwarm
29
30
  params[:required]&.delete(CTX_VARS_NAME.to_sym)
30
31
  end
31
32
 
33
+ cleaned_messages = OpenAISwarm::Util.clean_message_tools(messages, agent.noisy_tool_calls)
34
+
32
35
  create_params = {
33
36
  model: model_override || agent.model,
34
- messages: messages,
37
+ messages: cleaned_messages,
35
38
  tools: tools.empty? ? nil : tools,
36
39
  }
37
40
 
@@ -39,6 +42,7 @@ module OpenAISwarm
39
42
  # create_params[:functions] = tools unless tools.empty?
40
43
  # create_params[:function_call] = agent.tool_choice if agent.tool_choice
41
44
 
45
+ create_params[:temperature] = agent.temperature if agent.temperature
42
46
  create_params[:tool_choice] = agent.tool_choice if agent.tool_choice
43
47
  create_params[:parallel_tool_calls] = agent.parallel_tool_calls if tools.any?
44
48
 
@@ -144,6 +148,7 @@ module OpenAISwarm
144
148
  end
145
149
 
146
150
  def run(agent:, messages:, context_variables: {}, model_override: nil, stream: false, debug: false, max_turns: Float::INFINITY, execute_tools: true)
151
+ agent_tracker = OpenAISwarm::AgentChangeTracker.new(agent)
147
152
  if stream
148
153
  return run_and_stream(
149
154
  agent: agent,
@@ -162,8 +167,11 @@ module OpenAISwarm
162
167
  init_len = messages.length
163
168
 
164
169
  while history.length - init_len < max_turns && active_agent
170
+ agent_tracker.update(active_agent)
171
+ history = [history.first] if agent_tracker.switch_agent_reset_message?
172
+
165
173
  completion = get_chat_completion(
166
- active_agent,
174
+ agent_tracker,
167
175
  history,
168
176
  context_variables,
169
177
  model_override,
@@ -202,16 +210,21 @@ module OpenAISwarm
202
210
  )
203
211
  end
204
212
 
213
+ # TODO(Grayson): a lot of copied code here that will be refactored
205
214
  def run_and_stream(agent:, messages:, context_variables: {}, model_override: nil, debug: false, max_turns: Float::INFINITY, execute_tools: true)
215
+ agent_tracker = OpenAISwarm::AgentChangeTracker.new(agent)
206
216
  active_agent = agent
207
217
  context_variables = context_variables.dup
208
218
  history = messages.dup
209
219
  init_len = messages.length
210
220
 
211
221
  while history.length - init_len < max_turns && active_agent
222
+ agent_tracker.update(active_agent)
223
+ history = [history.first] if agent_tracker.switch_agent_reset_message?
224
+
212
225
  message = OpenAISwarm::Util.message_template(agent.name)
213
226
  completion = get_chat_completion(
214
- active_agent,
227
+ agent_tracker,
215
228
  history,
216
229
  context_variables,
217
230
  model_override,
@@ -0,0 +1,16 @@
1
+ # Backport of Array.wrap for Ruby versions prior to 3.0
2
+ # This provides a consistent way to wrap objects as arrays across different Ruby versions
3
+ # link: https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/array/wrap.rb
4
+ unless Array.respond_to?(:wrap)
5
+ class Array
6
+ def self.wrap(object)
7
+ if object.nil?
8
+ []
9
+ elsif object.respond_to?(:to_ary)
10
+ object.to_ary || [object]
11
+ else
12
+ [object]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -7,6 +7,50 @@ module OpenAISwarm
7
7
  puts "\e[97m[\e[90m#{timestamp}\e[97m]\e[90m #{message}\e[0m"
8
8
  end
9
9
 
10
+ def self.symbolize_keys_to_string(obj)
11
+ case obj
12
+ when Hash
13
+ obj.transform_keys(&:to_s).transform_values { |v| symbolize_keys_to_string(v) }
14
+ when Array
15
+ obj.map { |v| symbolize_keys_to_string(v) }
16
+ else
17
+ obj
18
+ end
19
+ end
20
+
21
+ def self.clean_message_tools(messages, tool_names)
22
+ return messages if tool_names.empty?
23
+ filtered_messages = symbolize_keys_to_string(messages.dup)
24
+ # Collect tool call IDs to be removed
25
+ tool_call_ids_to_remove = filtered_messages
26
+ .select { |msg| msg['tool_calls'] }
27
+ .flat_map { |msg| msg['tool_calls'] }
28
+ .select { |tool_call| tool_names.include?(tool_call['function']['name']) }
29
+ .map { |tool_call| tool_call['id'] }
30
+
31
+ # Remove specific messages
32
+ filtered_messages.reject! do |msg|
33
+ # Remove tool call messages for specified tool names
34
+ (msg['role'] == 'assistant' &&
35
+ msg['tool_calls']&.all? { |tool_call| tool_names.include?(tool_call['function']['name']) }) ||
36
+ # Remove tool response messages for specified tool calls
37
+ (msg['role'] == 'tool' && tool_call_ids_to_remove.include?(msg['tool_call_id']))
38
+ end
39
+
40
+ # If assistant message's tool_calls becomes empty, modify that message
41
+ filtered_messages.map! do |msg|
42
+ if msg['role'] == 'assistant' && msg['tool_calls']
43
+ msg['tool_calls'].reject! { |tool_call| tool_names.include?(tool_call['function']['name']) }
44
+ msg['tool_calls'] = nil if msg['tool_calls'].empty?
45
+ msg
46
+ else
47
+ msg
48
+ end
49
+ end
50
+
51
+ filtered_messages
52
+ end
53
+
10
54
  def self.message_template(agent_name)
11
55
  {
12
56
  "content" => "",
@@ -1,3 +1,3 @@
1
1
  module OpenAISwarm
2
- VERSION = "0.2.8"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,5 +1,8 @@
1
1
  require 'ruby-openai-swarm/version'
2
+ require 'ruby-openai-swarm/core_ext'
2
3
  require 'ruby-openai-swarm/agent'
4
+ require 'ruby-openai-swarm/agent_change_tracker'
5
+ require 'ruby-openai-swarm/agent_strategy_options'
3
6
  require 'ruby-openai-swarm/response'
4
7
  require 'ruby-openai-swarm/result'
5
8
  require 'ruby-openai-swarm/util'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-openai-swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grayson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-12 00:00:00.000000000 Z
11
+ date: 2024-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai
@@ -106,8 +106,11 @@ files:
106
106
  - examples/weather_agent/run.rb
107
107
  - lib/ruby-openai-swarm.rb
108
108
  - lib/ruby-openai-swarm/agent.rb
109
+ - lib/ruby-openai-swarm/agent_change_tracker.rb
110
+ - lib/ruby-openai-swarm/agent_strategy_options.rb
109
111
  - lib/ruby-openai-swarm/configuration.rb
110
112
  - lib/ruby-openai-swarm/core.rb
113
+ - lib/ruby-openai-swarm/core_ext.rb
111
114
  - lib/ruby-openai-swarm/function_descriptor.rb
112
115
  - lib/ruby-openai-swarm/logger.rb
113
116
  - lib/ruby-openai-swarm/repl.rb