From ab8b2fa950372cbba62377a726c1007d343794de Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Tue, 11 Feb 2025 15:34:41 +0800 Subject: [PATCH 01/11] first version for memory save temp --- examples/memory/memory_save.rb | 74 +++++++++++++++++++ lib/ruby-openai-swarm.rb | 3 + lib/ruby-openai-swarm/agent.rb | 13 +++- lib/ruby-openai-swarm/core.rb | 30 +++++++- .../functions/core_memory_function.rb | 46 ++++++++++++ lib/ruby-openai-swarm/memory.rb | 52 +++++++++++++ 6 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 examples/memory/memory_save.rb create mode 100644 lib/ruby-openai-swarm/functions/core_memory_function.rb create mode 100644 lib/ruby-openai-swarm/memory.rb diff --git a/examples/memory/memory_save.rb b/examples/memory/memory_save.rb new file mode 100644 index 0000000..48c6fc2 --- /dev/null +++ b/examples/memory/memory_save.rb @@ -0,0 +1,74 @@ +require_relative "../bootstrap" + +client = OpenAISwarm.new +model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o-mini" +# model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o" +memory_fields = ["language", "grade", "name", "sex"] +memory = OpenAISwarm::Memory.new({ memory_fields: memory_fields }) +memory.function + +def get_weather(location:) + "{'temp':67, 'unit':'F'}" +end + +def get_news(category:) + [ + "Tech Company A Acquires Startup B", + "New AI Model Revolutionizes Industry", + "Breakthrough in Quantum Computing" + ].sample +end + +get_news_instance = OpenAISwarm::FunctionDescriptor.new( + target_method: :get_news, + description: 'Get the latest news headlines. The category of news, e.g., world, business, sports.' +) + +get_weather_instance = OpenAISwarm::FunctionDescriptor.new( + target_method: :get_weather, + description: 'Simulate fetching weather data' +) + + +system_prompt = "You are a helpful teaching assistant. Remember to save important information about the student using the core_memory_save function. Always greet the student by name if you know it." + +chatbot_agent = OpenAISwarm::Agent.new( + name: "teaching_assistant", + instructions: system_prompt, + model: model, + functions: [ + get_weather_instance, + get_news_instance + ], + memory: memory +) + +puts ">>>>>>>>>>>>" +messages1 = [ + { + "role": "user", + "content": "Hi, I'm John. I speak Chinese and I'm in Senior Year." + } +] + +puts "first call, set memory" +puts "messages: #{messages1}" + +response1 = client.run(agent: chatbot_agent, messages: messages1, debug: env_debug) +puts memory.memories +puts response1.messages.last['content'] + +puts ">>>>>>>>>>>>" + + +messages2 = [ + {"role": "user", "content": "what is my name"}, +] +puts "2nd call, get memory" +puts "messages: #{messages2}" + +response2 = client.run(agent: chatbot_agent, messages: messages2, debug: env_debug) + +puts response2.messages.last['content'] + +# binding.pry diff --git a/lib/ruby-openai-swarm.rb b/lib/ruby-openai-swarm.rb index 4acee22..bfc700a 100644 --- a/lib/ruby-openai-swarm.rb +++ b/lib/ruby-openai-swarm.rb @@ -11,6 +11,9 @@ require 'ruby-openai-swarm/repl' require 'ruby-openai-swarm/configuration' require 'ruby-openai-swarm/logger' +require 'ruby-openai-swarm/memory' +require 'ruby-openai-swarm/functions/core_memory_function' + module OpenAISwarm class Error < StandardError; diff --git a/lib/ruby-openai-swarm/agent.rb b/lib/ruby-openai-swarm/agent.rb index 217f82a..617926e 100644 --- a/lib/ruby-openai-swarm/agent.rb +++ b/lib/ruby-openai-swarm/agent.rb @@ -7,7 +7,8 @@ class Agent :noisy_tool_calls, :temperature, :current_tool_name, - :resource + :resource, + :memory # These attributes can be read and written externally. They include: # - name: The name of the agent. # - model: The model used, e.g., "gpt-4". @@ -27,7 +28,8 @@ def initialize( resource: nil, noisy_tool_calls: [], strategy: {}, - current_tool_name: nil + current_tool_name: nil, + memory: nil ) @name = name @model = model @@ -40,6 +42,13 @@ def initialize( @noisy_tool_calls = noisy_tool_calls @strategy = Agents::StrategyOptions.new(strategy) @current_tool_name = current_tool_name.nil? ? nil : current_tool_name.to_s + @memory = memory + end + + def functions + return @functions if memory&.function.nil? + + @functions.push(memory.function) end end end diff --git a/lib/ruby-openai-swarm/core.rb b/lib/ruby-openai-swarm/core.rb index f795078..358f3be 100644 --- a/lib/ruby-openai-swarm/core.rb +++ b/lib/ruby-openai-swarm/core.rb @@ -10,16 +10,38 @@ class Core include Util CTX_VARS_NAME = 'context_variables' - def initialize(client = nil) + def initialize(client = nil, memory_fields = []) @client = client || OpenAI::Client.new @logger = OpenAISwarm::Logger.instance.logger + # @memory_fields = memory_fields + end + + def create_agent(name:, model:, instructions:, **options) + memory = Memory.new(@memory_fields) + memory_function = Functions::CoreMemoryFunction.new(agent, @memory_fields) + + functions = options[:functions] || [] + functions << memory_function + + Agent.new( + name: name, + model: model, + instructions: instructions, + memory: memory, + functions: functions, + **options + ) end def get_chat_completion(agent_tracker, history, context_variables, model_override, stream, debug) agent = agent_tracker.current_agent context_variables = context_variables.dup instructions = agent.instructions.respond_to?(:call) ? agent.instructions.call(context_variables) : agent.instructions - messages = [{ role: 'system', content: instructions }] + history + + # Build a message history, including memories + messages = [{ role: 'system', content: instructions }] + messages << { role: 'system', content: agent.memory.prompt_content } unless agent&.memory&.prompt_content.nil? + messages += history # Util.debug_print(debug, "Getting chat completion for...:", messages) @@ -182,11 +204,11 @@ def run(agent:, messages:, context_variables: {}, model_override: nil, stream: f debug ) - message = completion.dig('choices', 0, 'message') + message = completion.dig('choices', 0, 'message') || {} Util.debug_print(debug, "Received completion:", message) log_message(:info, "Received completion:", message) - message['sender'] = active_agent.name + message['sender'] = active_agent&.name history << message if !message['tool_calls'] || !execute_tools diff --git a/lib/ruby-openai-swarm/functions/core_memory_function.rb b/lib/ruby-openai-swarm/functions/core_memory_function.rb new file mode 100644 index 0000000..019d30c --- /dev/null +++ b/lib/ruby-openai-swarm/functions/core_memory_function.rb @@ -0,0 +1,46 @@ +module OpenAISwarm + module Functions + class CoreMemoryFunction + def self.definition(memory_fields = []) + properties = { + section: { + type: "string", + enum: ["human", "agent"], + description: "Must be either 'human' (to save information about the human) or 'agent'(to save information about yourself)" + } + } + + memory_fields.each do |field| + properties[field] = { + type: "string", + description: "The #{field} to remember" + } + end + + { + type: "function", + function: { + name: "core_memory_save", + description: "Save important information about you, the agent or the human you are chatting with.", + parameters: { + type: "object", + properties: properties, + # required: ["section"] + memory_fields + } + } + } + end + + def initialize(agent, memory_fields = []) + @agent = agent + @memory_fields = memory_fields + end + + def call(section:, **memories) + memory_data = memories.transform_keys(&:to_s) + @agent.memory.add(section, memory_data) + "Memory saved in #{section} section: #{memory_data}" + end + end + end +end \ No newline at end of file diff --git a/lib/ruby-openai-swarm/memory.rb b/lib/ruby-openai-swarm/memory.rb new file mode 100644 index 0000000..0ce9cb6 --- /dev/null +++ b/lib/ruby-openai-swarm/memory.rb @@ -0,0 +1,52 @@ +module OpenAISwarm + class Memory + attr_reader :memories + + def initialize(memory_fields: nil) + @memories = {} + @memory_fields = memory_fields + end + + def core_memory_save(args) + puts "core_memory_save" + args.each { |key, value| add(key, value) } + # @memories[section].merge!(memory) + end + + def prompt_content + return nil if memories.empty? + + "You have a section of your context called [MEMORY] " \ + "that contains information relevant to your conversation [MEMORY]\n" \ + "#{get_memories_data}" + end + + def function + return nil if @memory_fields.empty? + core_memory_save_metadata = Functions::CoreMemoryFunction.definition(@memory_fields) + description = core_memory_save_metadata[:function][:description] + parameters = core_memory_save_metadata[:function][:parameters] + OpenAISwarm::FunctionDescriptor.new( + target_method: method(:core_memory_save), + description: description, + parameters: parameters + ) + end + + def add(section, memory) + @memories[section] = memory + end + + def get(section) + @memories[section] + end + + def clear(section = nil) + @memories = {} + end + + def get_memories_data + memories.to_json + end + end +end From dab280771e5d5dd4f21f43da8056cb6c655ec815 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Fri, 14 Feb 2025 16:18:03 +0800 Subject: [PATCH 02/11] add memoryies entiry --- examples/memory/memory_save.rb | 12 ++++- lib/ruby-openai-swarm.rb | 1 + .../memories/entity_store.rb | 54 +++++++++++++++++++ lib/ruby-openai-swarm/memory.rb | 29 +++------- 4 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 lib/ruby-openai-swarm/memories/entity_store.rb diff --git a/examples/memory/memory_save.rb b/examples/memory/memory_save.rb index 48c6fc2..fbe936a 100644 --- a/examples/memory/memory_save.rb +++ b/examples/memory/memory_save.rb @@ -8,10 +8,12 @@ memory.function def get_weather(location:) + puts "tool call: get_weather" "{'temp':67, 'unit':'F'}" end def get_news(category:) + puts "tool call: get_news" [ "Tech Company A Acquires Startup B", "New AI Model Revolutionizes Industry", @@ -44,10 +46,17 @@ def get_news(category:) ) puts ">>>>>>>>>>>>" +# messages1 = [ +# { +# "role": "user", +# "content": "Hi, I'm John. I speak Chinese and I'm in Senior Year. " +# } +# ] + messages1 = [ { "role": "user", - "content": "Hi, I'm John. I speak Chinese and I'm in Senior Year." + "content": "Hi, I'm John. I speak Chinese and I'm in Senior Year. Get the current weather in a given location. Location MUST be a city." } ] @@ -58,6 +67,7 @@ def get_news(category:) puts memory.memories puts response1.messages.last['content'] +puts "" puts ">>>>>>>>>>>>" diff --git a/lib/ruby-openai-swarm.rb b/lib/ruby-openai-swarm.rb index bfc700a..e292617 100644 --- a/lib/ruby-openai-swarm.rb +++ b/lib/ruby-openai-swarm.rb @@ -12,6 +12,7 @@ require 'ruby-openai-swarm/configuration' require 'ruby-openai-swarm/logger' require 'ruby-openai-swarm/memory' +require 'ruby-openai-swarm/memories/entity_store' require 'ruby-openai-swarm/functions/core_memory_function' diff --git a/lib/ruby-openai-swarm/memories/entity_store.rb b/lib/ruby-openai-swarm/memories/entity_store.rb new file mode 100644 index 0000000..966f3fe --- /dev/null +++ b/lib/ruby-openai-swarm/memories/entity_store.rb @@ -0,0 +1,54 @@ +module OpenAISwarm + module Memories + class EntityStore + attr_accessor :entity_store, + :data + + def initialize(entity_store = nil) + @entity_store = entity_store + @data = entity_store&.data || {} + end + + def entity_store_save + return unless entity_store.respond_to?(:update) + + entity_store.update(data: data) + end + + def add_entities(entities) + entities.each { |key, value| @data[key.to_sym] = value } + entity_store_save + end + + def memories + data&.to_json + end + + # def add(key, value) + # @data[key] = value + # entity_store_save + # @data + # end + + # def get(key) + # @data[key] + # end + + # def exists?(key) + # @data.key?(key) + # end + + # def remove(key) + # @data.delete(key) + # end + + # def clear + # @data = {} + # end + + # def all + # @data + # end + end + end +end diff --git a/lib/ruby-openai-swarm/memory.rb b/lib/ruby-openai-swarm/memory.rb index 0ce9cb6..401b0b8 100644 --- a/lib/ruby-openai-swarm/memory.rb +++ b/lib/ruby-openai-swarm/memory.rb @@ -1,20 +1,19 @@ module OpenAISwarm class Memory - attr_reader :memories + attr_reader :memories, + :entity_store - def initialize(memory_fields: nil) - @memories = {} + def initialize(memory_fields: nil, entity_store: nil) @memory_fields = memory_fields + @entity_store = Memories::EntityStore.new(entity_store) end - def core_memory_save(args) - puts "core_memory_save" - args.each { |key, value| add(key, value) } - # @memories[section].merge!(memory) + def core_memory_save(entities) + entity_store.add_entities(entities) end def prompt_content - return nil if memories.empty? + return nil if get_memories_data.nil? "You have a section of your context called [MEMORY] " \ "that contains information relevant to your conversation [MEMORY]\n" \ @@ -33,20 +32,8 @@ def function ) end - def add(section, memory) - @memories[section] = memory - end - - def get(section) - @memories[section] - end - - def clear(section = nil) - @memories = {} - end - def get_memories_data - memories.to_json + entity_store&.memories end end end From 6589d382890920e00a837a817183cfd81063431e Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 10:18:43 +0800 Subject: [PATCH 03/11] refactor --- examples/memory/memory_save.rb | 9 --------- lib/ruby-openai-swarm.rb | 2 +- lib/ruby-openai-swarm/core.rb | 2 +- .../{functions => memories}/core_memory_function.rb | 4 ++-- lib/ruby-openai-swarm/memory.rb | 12 +++++++----- 5 files changed, 11 insertions(+), 18 deletions(-) rename lib/ruby-openai-swarm/{functions => memories}/core_memory_function.rb (98%) diff --git a/examples/memory/memory_save.rb b/examples/memory/memory_save.rb index fbe936a..f323c34 100644 --- a/examples/memory/memory_save.rb +++ b/examples/memory/memory_save.rb @@ -2,7 +2,6 @@ client = OpenAISwarm.new model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o-mini" -# model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o" memory_fields = ["language", "grade", "name", "sex"] memory = OpenAISwarm::Memory.new({ memory_fields: memory_fields }) memory.function @@ -46,12 +45,6 @@ def get_news(category:) ) puts ">>>>>>>>>>>>" -# messages1 = [ -# { -# "role": "user", -# "content": "Hi, I'm John. I speak Chinese and I'm in Senior Year. " -# } -# ] messages1 = [ { @@ -80,5 +73,3 @@ def get_news(category:) response2 = client.run(agent: chatbot_agent, messages: messages2, debug: env_debug) puts response2.messages.last['content'] - -# binding.pry diff --git a/lib/ruby-openai-swarm.rb b/lib/ruby-openai-swarm.rb index e292617..7679061 100644 --- a/lib/ruby-openai-swarm.rb +++ b/lib/ruby-openai-swarm.rb @@ -13,7 +13,7 @@ require 'ruby-openai-swarm/logger' require 'ruby-openai-swarm/memory' require 'ruby-openai-swarm/memories/entity_store' -require 'ruby-openai-swarm/functions/core_memory_function' +require 'ruby-openai-swarm/memories/core_memory_function' module OpenAISwarm diff --git a/lib/ruby-openai-swarm/core.rb b/lib/ruby-openai-swarm/core.rb index 358f3be..797855c 100644 --- a/lib/ruby-openai-swarm/core.rb +++ b/lib/ruby-openai-swarm/core.rb @@ -18,7 +18,7 @@ def initialize(client = nil, memory_fields = []) def create_agent(name:, model:, instructions:, **options) memory = Memory.new(@memory_fields) - memory_function = Functions::CoreMemoryFunction.new(agent, @memory_fields) + memory_function = Memories::CoreMemoryFunction.new(agent, @memory_fields) functions = options[:functions] || [] functions << memory_function diff --git a/lib/ruby-openai-swarm/functions/core_memory_function.rb b/lib/ruby-openai-swarm/memories/core_memory_function.rb similarity index 98% rename from lib/ruby-openai-swarm/functions/core_memory_function.rb rename to lib/ruby-openai-swarm/memories/core_memory_function.rb index 019d30c..6d7126f 100644 --- a/lib/ruby-openai-swarm/functions/core_memory_function.rb +++ b/lib/ruby-openai-swarm/memories/core_memory_function.rb @@ -1,5 +1,5 @@ module OpenAISwarm - module Functions + module Memories class CoreMemoryFunction def self.definition(memory_fields = []) properties = { @@ -43,4 +43,4 @@ def call(section:, **memories) end end end -end \ No newline at end of file +end diff --git a/lib/ruby-openai-swarm/memory.rb b/lib/ruby-openai-swarm/memory.rb index 401b0b8..e7d4bb5 100644 --- a/lib/ruby-openai-swarm/memory.rb +++ b/lib/ruby-openai-swarm/memory.rb @@ -22,16 +22,18 @@ def prompt_content def function return nil if @memory_fields.empty? - core_memory_save_metadata = Functions::CoreMemoryFunction.definition(@memory_fields) - description = core_memory_save_metadata[:function][:description] - parameters = core_memory_save_metadata[:function][:parameters] + OpenAISwarm::FunctionDescriptor.new( target_method: method(:core_memory_save), - description: description, - parameters: parameters + description: core_memory_save_metadata[:function][:description], + parameters: core_memory_save_metadata[:function][:parameters] ) end + def core_memory_save_metadata + @core_memory_save_metadata ||= Memories::CoreMemoryFunction.definition(@memory_fields) + end + def get_memories_data entity_store&.memories end From e5ebdbe8ea78b4ef52375abd20ec23fbe02ad299 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 10:39:39 +0800 Subject: [PATCH 04/11] when model is nil raise respond body --- lib/ruby-openai-swarm/core.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ruby-openai-swarm/core.rb b/lib/ruby-openai-swarm/core.rb index 797855c..cada9cb 100644 --- a/lib/ruby-openai-swarm/core.rb +++ b/lib/ruby-openai-swarm/core.rb @@ -88,9 +88,10 @@ def get_chat_completion(agent_tracker, history, context_variables, model_overrid Util.debug_print(debug, "API Response:", response) response - rescue OpenAI::Error => e - log_message(:error, "OpenAI API Error: #{e.message}") - Util.debug_print(true, "OpenAI API Error:", e.message) + rescue OpenAI::Error, Faraday::BadRequestError => e + error_message = (e.response || {}).dig(:body) || e.inspect + log_message(:error, "OpenAI API Error: #{error_message}") + Util.debug_print(true, "OpenAI API Error:", error_message) raise end From e023e06ddf030bf5a3cf428e482a30ed1985b395 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 11:22:12 +0800 Subject: [PATCH 05/11] clean code --- examples/memory/memory_save.rb | 15 +++++---- lib/ruby-openai-swarm/core.rb | 31 ++++++++----------- .../memories/core_memory_function.rb | 20 +----------- lib/ruby-openai-swarm/memory.rb | 4 ++- lib/ruby-openai-swarm/version.rb | 2 +- 5 files changed, 25 insertions(+), 47 deletions(-) diff --git a/examples/memory/memory_save.rb b/examples/memory/memory_save.rb index f323c34..0a3e65a 100644 --- a/examples/memory/memory_save.rb +++ b/examples/memory/memory_save.rb @@ -2,8 +2,8 @@ client = OpenAISwarm.new model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o-mini" -memory_fields = ["language", "grade", "name", "sex"] -memory = OpenAISwarm::Memory.new({ memory_fields: memory_fields }) + +memory = OpenAISwarm::Memory.new({ memory_fields: ["language", "grade", "name", "sex"] }) memory.function def get_weather(location:) @@ -44,8 +44,6 @@ def get_news(category:) memory: memory ) -puts ">>>>>>>>>>>>" - messages1 = [ { "role": "user", @@ -57,19 +55,20 @@ def get_news(category:) puts "messages: #{messages1}" response1 = client.run(agent: chatbot_agent, messages: messages1, debug: env_debug) -puts memory.memories +puts "memory data: #{memory.entity_store.data}" puts response1.messages.last['content'] puts "" -puts ">>>>>>>>>>>>" - - messages2 = [ {"role": "user", "content": "what is my name"}, ] puts "2nd call, get memory" +puts "memory data: #{memory.entity_store.data}" puts "messages: #{messages2}" response2 = client.run(agent: chatbot_agent, messages: messages2, debug: env_debug) puts response2.messages.last['content'] + +# memory.entity_store.data +# binding.pry diff --git a/lib/ruby-openai-swarm/core.rb b/lib/ruby-openai-swarm/core.rb index cada9cb..d0b5d61 100644 --- a/lib/ruby-openai-swarm/core.rb +++ b/lib/ruby-openai-swarm/core.rb @@ -10,28 +10,23 @@ class Core include Util CTX_VARS_NAME = 'context_variables' - def initialize(client = nil, memory_fields = []) + def initialize(client = nil) @client = client || OpenAI::Client.new @logger = OpenAISwarm::Logger.instance.logger - # @memory_fields = memory_fields end - def create_agent(name:, model:, instructions:, **options) - memory = Memory.new(@memory_fields) - memory_function = Memories::CoreMemoryFunction.new(agent, @memory_fields) - - functions = options[:functions] || [] - functions << memory_function - - Agent.new( - name: name, - model: model, - instructions: instructions, - memory: memory, - functions: functions, - **options - ) - end + # TODO(Grayson) + # def create_agent(name:, model:, instructions:, **options) + # memory = Memory.new(@memory_fields) + # Agent.new( + # name: name, + # model: model, + # instructions: instructions, + # memory: memory, + # functions: functions, + # **options + # ) + # end def get_chat_completion(agent_tracker, history, context_variables, model_override, stream, debug) agent = agent_tracker.current_agent diff --git a/lib/ruby-openai-swarm/memories/core_memory_function.rb b/lib/ruby-openai-swarm/memories/core_memory_function.rb index 6d7126f..ba7b03a 100644 --- a/lib/ruby-openai-swarm/memories/core_memory_function.rb +++ b/lib/ruby-openai-swarm/memories/core_memory_function.rb @@ -2,13 +2,7 @@ module OpenAISwarm module Memories class CoreMemoryFunction def self.definition(memory_fields = []) - properties = { - section: { - type: "string", - enum: ["human", "agent"], - description: "Must be either 'human' (to save information about the human) or 'agent'(to save information about yourself)" - } - } + properties = {} memory_fields.each do |field| properties[field] = { @@ -25,22 +19,10 @@ def self.definition(memory_fields = []) parameters: { type: "object", properties: properties, - # required: ["section"] + memory_fields } } } end - - def initialize(agent, memory_fields = []) - @agent = agent - @memory_fields = memory_fields - end - - def call(section:, **memories) - memory_data = memories.transform_keys(&:to_s) - @agent.memory.add(section, memory_data) - "Memory saved in #{section} section: #{memory_data}" - end end end end diff --git a/lib/ruby-openai-swarm/memory.rb b/lib/ruby-openai-swarm/memory.rb index e7d4bb5..b92e7af 100644 --- a/lib/ruby-openai-swarm/memory.rb +++ b/lib/ruby-openai-swarm/memory.rb @@ -15,8 +15,10 @@ def core_memory_save(entities) def prompt_content return nil if get_memories_data.nil? + fields = @memory_fields.join(", ") "You have a section of your context called [MEMORY] " \ - "that contains information relevant to your conversation [MEMORY]\n" \ + "that contains the following information: #{fields}. " \ + "Here are the relevant details: [MEMORY]\n" \ "#{get_memories_data}" end diff --git a/lib/ruby-openai-swarm/version.rb b/lib/ruby-openai-swarm/version.rb index bc46c50..80b8916 100644 --- a/lib/ruby-openai-swarm/version.rb +++ b/lib/ruby-openai-swarm/version.rb @@ -1,3 +1,3 @@ module OpenAISwarm - VERSION = "0.4.0.2" + VERSION = "0.5.0" end From 6b0b9ee6d0e77e79007765f03e386e2beb0ce37a Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 16:31:26 +0800 Subject: [PATCH 06/11] Add Memories Field --- lib/ruby-openai-swarm.rb | 1 + .../memories/core_memory_function.rb | 9 ++--- lib/ruby-openai-swarm/memories/field.rb | 39 +++++++++++++++++++ lib/ruby-openai-swarm/memory.rb | 12 ++++-- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 lib/ruby-openai-swarm/memories/field.rb diff --git a/lib/ruby-openai-swarm.rb b/lib/ruby-openai-swarm.rb index 7679061..9ad1352 100644 --- a/lib/ruby-openai-swarm.rb +++ b/lib/ruby-openai-swarm.rb @@ -14,6 +14,7 @@ require 'ruby-openai-swarm/memory' require 'ruby-openai-swarm/memories/entity_store' require 'ruby-openai-swarm/memories/core_memory_function' +require 'ruby-openai-swarm/memories/field' module OpenAISwarm diff --git a/lib/ruby-openai-swarm/memories/core_memory_function.rb b/lib/ruby-openai-swarm/memories/core_memory_function.rb index ba7b03a..5992480 100644 --- a/lib/ruby-openai-swarm/memories/core_memory_function.rb +++ b/lib/ruby-openai-swarm/memories/core_memory_function.rb @@ -4,11 +4,10 @@ class CoreMemoryFunction def self.definition(memory_fields = []) properties = {} - memory_fields.each do |field| - properties[field] = { - type: "string", - description: "The #{field} to remember" - } + memory_fields.each do |memory_field| + field = memory_field.field + description = "The #{field} to remember." + memory_field&.tool_call_description.to_s + properties[field] = { type: "string", description: description } end { diff --git a/lib/ruby-openai-swarm/memories/field.rb b/lib/ruby-openai-swarm/memories/field.rb new file mode 100644 index 0000000..c59e2af --- /dev/null +++ b/lib/ruby-openai-swarm/memories/field.rb @@ -0,0 +1,39 @@ +module OpenAISwarm + module Memories + class Field + attr_accessor :field, + :tool_call_description + + VALID_MEMORY_FIELDS = [:field, :tool_call_description].freeze + + def initialize(memory_field) + memory_field.is_a?(Hash) ? parse_hash(memory_field) : parse_string(memory_field) + end + + def parse_hash(memory_field) + validate_memory_field!(memory_field) + + @field = memory_field[:field] + @tool_call_description = memory_field[:tool_call_description] + end + + def parse_string(memory_field) + @field = memory_field + end + + private + + def validate_memory_field!(memory_field) + unless memory_field.include?(:field) + raise ArgumentError, "memory_field must include :field" + end + + invalid_fields = memory_field.keys - VALID_MEMORY_FIELDS + + unless invalid_fields.empty? + raise ArgumentError, "Invalid memory fields: #{invalid_fields.join(', ')}. Valid fields are: #{VALID_MEMORY_FIELDS.join(', ')}" + end + end + end + end +end diff --git a/lib/ruby-openai-swarm/memory.rb b/lib/ruby-openai-swarm/memory.rb index b92e7af..a463748 100644 --- a/lib/ruby-openai-swarm/memory.rb +++ b/lib/ruby-openai-swarm/memory.rb @@ -3,11 +3,17 @@ class Memory attr_reader :memories, :entity_store - def initialize(memory_fields: nil, entity_store: nil) - @memory_fields = memory_fields + def initialize(memory_fields: [], entity_store: nil) + @memory_fields = normalize_memory_fields(memory_fields) @entity_store = Memories::EntityStore.new(entity_store) end + def normalize_memory_fields(memory_fields) + return [] if memory_fields.empty? + + memory_fields.map { |memory_field| Memories::Field.new(memory_field) } + end + def core_memory_save(entities) entity_store.add_entities(entities) end @@ -15,7 +21,7 @@ def core_memory_save(entities) def prompt_content return nil if get_memories_data.nil? - fields = @memory_fields.join(", ") + fields = @memory_fields.map(&:field).join(", ") "You have a section of your context called [MEMORY] " \ "that contains the following information: #{fields}. " \ "Here are the relevant details: [MEMORY]\n" \ From 64285e46a3bd6d35808c836ceca02d8ffcc41f0c Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 17:47:56 +0800 Subject: [PATCH 07/11] add Memories Field test case --- .../ruby-openai-swarm/memories/field_spec.rb | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 spec/lib/ruby-openai-swarm/memories/field_spec.rb diff --git a/spec/lib/ruby-openai-swarm/memories/field_spec.rb b/spec/lib/ruby-openai-swarm/memories/field_spec.rb new file mode 100644 index 0000000..6457353 --- /dev/null +++ b/spec/lib/ruby-openai-swarm/memories/field_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +RSpec.describe OpenAISwarm::Memories::Field do + # Test initialization with a string + describe '#initialize with string' do + it 'sets the field value when initialized with a string' do + field = described_class.new('test_field') + expect(field.field).to eq('test_field') + expect(field.tool_call_description).to be_nil + end + end + + # Test initialization with a valid hash + describe '#initialize with hash' do + it 'sets both field and tool_call_description when initialized with a valid hash' do + field = described_class.new( + field: 'test_field', + tool_call_description: 'test description' + ) + expect(field.field).to eq('test_field') + expect(field.tool_call_description).to eq('test description') + end + + it 'sets only field when tool_call_description is not provided' do + field = described_class.new(field: 'test_field') + expect(field.field).to eq('test_field') + expect(field.tool_call_description).to be_nil + end + end + + # Test validation errors + describe 'validation' do + it 'raises ArgumentError when :field is missing from hash' do + expect { + described_class.new(tool_call_description: 'test description') + }.to raise_error(ArgumentError, 'memory_field must include :field') + end + + it 'raises ArgumentError when invalid keys are present' do + expect { + described_class.new( + field: 'test_field', + invalid_key: 'value' + ) + }.to raise_error( + ArgumentError, + 'Invalid memory fields: invalid_key. Valid fields are: field, tool_call_description' + ) + end + end + + # Test parse_hash method + describe '#parse_hash' do + it 'correctly parses a valid hash' do + # Initialize with valid field to avoid validation error + field = described_class.new(field: 'initial') + + field.send(:parse_hash, { + field: 'test_field', + tool_call_description: 'test description' + }) + + expect(field.field).to eq('test_field') + expect(field.tool_call_description).to eq('test description') + end + end + + # Test parse_string method + describe '#parse_string' do + it 'correctly parses a string' do + field = described_class.new(field: 'initial') # Initialize with valid field + field.send(:parse_string, 'test_field') + expect(field.field).to eq('test_field') + end + end +end \ No newline at end of file From 969cf9d0765e7e8a0e21e15c04b5ff08ec34e132 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 17:52:03 +0800 Subject: [PATCH 08/11] add CoreMemoryFunction test case --- .../ruby-openai-swarm/memories/field_spec.rb | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/spec/lib/ruby-openai-swarm/memories/field_spec.rb b/spec/lib/ruby-openai-swarm/memories/field_spec.rb index 6457353..f83d176 100644 --- a/spec/lib/ruby-openai-swarm/memories/field_spec.rb +++ b/spec/lib/ruby-openai-swarm/memories/field_spec.rb @@ -1,6 +1,84 @@ require 'spec_helper' RSpec.describe OpenAISwarm::Memories::Field do + let(:field_name) { :test_field } + let(:field_value) { "test value" } + let(:field) { described_class.new(field_name, field_value) } + + describe '#initialize' do + it 'creates a field with name and value' do + expect(field.name).to eq(field_name) + expect(field.value).to eq(field_value) + end + + it 'accepts different types of values' do + number_field = described_class.new(:number, 42) + expect(number_field.value).to eq(42) + + array_field = described_class.new(:array, [1, 2, 3]) + expect(array_field.value).to eq([1, 2, 3]) + + hash_field = described_class.new(:hash, { key: 'value' }) + expect(hash_field.value).to eq({ key: 'value' }) + end + end + + describe '#to_h' do + it 'returns a hash representation of the field' do + expect(field.to_h).to eq({ + name: field_name, + value: field_value + }) + end + end + + describe '#==' do + it 'returns true when comparing identical fields' do + field2 = described_class.new(field_name, field_value) + expect(field).to eq(field2) + end + + it 'returns false when comparing fields with different names' do + field2 = described_class.new(:other_field, field_value) + expect(field).not_to eq(field2) + end + + it 'returns false when comparing fields with different values' do + field2 = described_class.new(field_name, "other value") + expect(field).not_to eq(field2) + end + end + + describe '#clone' do + it 'creates a deep copy of the field' do + cloned_field = field.clone + expect(cloned_field).to eq(field) + expect(cloned_field.object_id).not_to eq(field.object_id) + end + + it 'creates independent copies of complex values' do + array_field = described_class.new(:array, [1, 2, 3]) + cloned_field = array_field.clone + cloned_field.value << 4 + expect(array_field.value).to eq([1, 2, 3]) + expect(cloned_field.value).to eq([1, 2, 3, 4]) + end + end + + describe 'validation' do + it 'raises error when name is nil' do + expect { + described_class.new(nil, "value") + }.to raise_error(ArgumentError, "Field name cannot be nil") + end + + it 'raises error when name is empty' do + expect { + described_class.new("", "value") + }.to raise_error(ArgumentError, "Field name cannot be empty") + end + end + # Test initialization with a string describe '#initialize with string' do it 'sets the field value when initialized with a string' do From 575727f9410fd234d61f0f3aa13587d264b67d81 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 17:55:23 +0800 Subject: [PATCH 09/11] add CoreMemoryFunction,memory and test case --- .../memories/core_memory_function_spec.rb | 66 ++++++++++++ .../ruby-openai-swarm/memories/field_spec.rb | 78 -------------- spec/lib/ruby-openai-swarm/memory_spec.rb | 101 ++++++++++++++++++ 3 files changed, 167 insertions(+), 78 deletions(-) create mode 100644 spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb create mode 100644 spec/lib/ruby-openai-swarm/memory_spec.rb diff --git a/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb b/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb new file mode 100644 index 0000000..979413f --- /dev/null +++ b/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +RSpec.describe OpenAISwarm::Memories::CoreMemoryFunction do + describe '.definition' do + # Test with empty memory fields + context 'when no memory fields are provided' do + it 'returns a function definition with empty properties' do + result = described_class.definition([]) + + expect(result).to be_a(Hash) + expect(result[:type]).to eq('function') + expect(result[:function][:name]).to eq('core_memory_save') + expect(result[:function][:parameters][:properties]).to be_empty + end + end + + # Test with memory fields + context 'when memory fields are provided' do + let(:memory_field) do + field = OpenAISwarm::Memories::Field.new('name') + field.tool_call_description = ' This is used to store the name.' + field + end + + let(:memory_fields) { [memory_field] } + + it 'returns a function definition with correct properties' do + result = described_class.definition(memory_fields) + + expect(result).to be_a(Hash) + expect(result[:type]).to eq('function') + expect(result[:function]).to include( + name: 'core_memory_save', + description: 'Save important information about you, the agent or the human you are chatting with.' + ) + + # Check properties structure + properties = result[:function][:parameters][:properties] + expect(properties).to include('name') + expect(properties['name']).to include( + type: 'string', + description: 'The name to remember. This is used to store the name.' + ) + end + end + + # Test with multiple memory fields + context 'when multiple memory fields are provided' do + let(:memory_fields) do + [ + OpenAISwarm::Memories::Field.new('name'), + OpenAISwarm::Memories::Field.new('age') + ] + end + + it 'includes all fields in the properties' do + result = described_class.definition(memory_fields) + properties = result[:function][:parameters][:properties] + + expect(properties.keys).to contain_exactly('name', 'age') + expect(properties['name'][:type]).to eq('string') + expect(properties['age'][:type]).to eq('string') + end + end + end +end \ No newline at end of file diff --git a/spec/lib/ruby-openai-swarm/memories/field_spec.rb b/spec/lib/ruby-openai-swarm/memories/field_spec.rb index f83d176..6457353 100644 --- a/spec/lib/ruby-openai-swarm/memories/field_spec.rb +++ b/spec/lib/ruby-openai-swarm/memories/field_spec.rb @@ -1,84 +1,6 @@ require 'spec_helper' RSpec.describe OpenAISwarm::Memories::Field do - let(:field_name) { :test_field } - let(:field_value) { "test value" } - let(:field) { described_class.new(field_name, field_value) } - - describe '#initialize' do - it 'creates a field with name and value' do - expect(field.name).to eq(field_name) - expect(field.value).to eq(field_value) - end - - it 'accepts different types of values' do - number_field = described_class.new(:number, 42) - expect(number_field.value).to eq(42) - - array_field = described_class.new(:array, [1, 2, 3]) - expect(array_field.value).to eq([1, 2, 3]) - - hash_field = described_class.new(:hash, { key: 'value' }) - expect(hash_field.value).to eq({ key: 'value' }) - end - end - - describe '#to_h' do - it 'returns a hash representation of the field' do - expect(field.to_h).to eq({ - name: field_name, - value: field_value - }) - end - end - - describe '#==' do - it 'returns true when comparing identical fields' do - field2 = described_class.new(field_name, field_value) - expect(field).to eq(field2) - end - - it 'returns false when comparing fields with different names' do - field2 = described_class.new(:other_field, field_value) - expect(field).not_to eq(field2) - end - - it 'returns false when comparing fields with different values' do - field2 = described_class.new(field_name, "other value") - expect(field).not_to eq(field2) - end - end - - describe '#clone' do - it 'creates a deep copy of the field' do - cloned_field = field.clone - expect(cloned_field).to eq(field) - expect(cloned_field.object_id).not_to eq(field.object_id) - end - - it 'creates independent copies of complex values' do - array_field = described_class.new(:array, [1, 2, 3]) - cloned_field = array_field.clone - cloned_field.value << 4 - expect(array_field.value).to eq([1, 2, 3]) - expect(cloned_field.value).to eq([1, 2, 3, 4]) - end - end - - describe 'validation' do - it 'raises error when name is nil' do - expect { - described_class.new(nil, "value") - }.to raise_error(ArgumentError, "Field name cannot be nil") - end - - it 'raises error when name is empty' do - expect { - described_class.new("", "value") - }.to raise_error(ArgumentError, "Field name cannot be empty") - end - end - # Test initialization with a string describe '#initialize with string' do it 'sets the field value when initialized with a string' do diff --git a/spec/lib/ruby-openai-swarm/memory_spec.rb b/spec/lib/ruby-openai-swarm/memory_spec.rb new file mode 100644 index 0000000..a773506 --- /dev/null +++ b/spec/lib/ruby-openai-swarm/memory_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +RSpec.describe OpenAISwarm::Memory do + # Test initialization and memory field normalization + describe '#initialize' do + context 'when initialized with memory fields' do + let(:memory_fields) { ['name', 'age'] } + let(:memory) { described_class.new(memory_fields: memory_fields) } + + it 'creates normalized memory fields' do + expect(memory.instance_variable_get(:@memory_fields).size).to eq(2) + expect(memory.instance_variable_get(:@memory_fields).first).to be_a(OpenAISwarm::Memories::Field) + end + end + + context 'when initialized without memory fields' do + let(:memory) { described_class.new } + + it 'creates empty memory fields array' do + expect(memory.instance_variable_get(:@memory_fields)).to be_empty + end + end + end + + # Test core memory save functionality + describe '#core_memory_save' do + let(:memory) { described_class.new(memory_fields: ['name']) } + let(:entities) { [{ 'name' => 'John Doe' }] } + + it 'delegates entity addition to entity store' do + expect(memory.entity_store).to receive(:add_entities).with(entities) + memory.core_memory_save(entities) + end + end + + # Test prompt content generation + describe '#prompt_content' do + context 'when memories exist' do + let(:memory_fields) { ['name', 'age'] } + let(:memory) { described_class.new(memory_fields: memory_fields) } + let(:memories_data) { "John Doe, 30" } + + before do + allow(memory).to receive(:get_memories_data).and_return(memories_data) + end + + it 'returns formatted prompt content with memories' do + expected_content = "You have a section of your context called [MEMORY] " \ + "that contains the following information: name, age. " \ + "Here are the relevant details: [MEMORY]\n" \ + "John Doe, 30" + expect(memory.prompt_content).to eq(expected_content) + end + end + + context 'when no memories exist' do + let(:memory) { described_class.new } + + before do + allow(memory).to receive(:get_memories_data).and_return(nil) + end + + it 'returns nil' do + expect(memory.prompt_content).to be_nil + end + end + end + + # Test function generation + describe '#function' do + context 'when memory fields exist' do + let(:memory) { described_class.new(memory_fields: ['name']) } + + it 'returns a FunctionDescriptor instance' do + expect(memory.function).to be_a(OpenAISwarm::FunctionDescriptor) + end + + it 'has correct target method' do + expect(memory.function.target_method).to eq(memory.method(:core_memory_save)) + end + end + + context 'when no memory fields exist' do + let(:memory) { described_class.new } + + it 'returns nil' do + expect(memory.function).to be_nil + end + end + end + + # Test memories data retrieval + describe '#get_memories_data' do + let(:memory) { described_class.new } + + it 'delegates to entity store' do + expect(memory.entity_store).to receive(:memories) + memory.get_memories_data + end + end +end \ No newline at end of file From 8006effe5eff23a36c36dad41e4a6a03581e43e5 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 19:40:00 +0800 Subject: [PATCH 10/11] format code --- .../memories/core_memory_function_spec.rb | 8 ++++---- spec/lib/ruby-openai-swarm/memories/field_spec.rb | 6 +++--- spec/lib/ruby-openai-swarm/memory_spec.rb | 2 +- .../util/latest_role_user_message_spec.rb | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb b/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb index 979413f..35dbbee 100644 --- a/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb +++ b/spec/lib/ruby-openai-swarm/memories/core_memory_function_spec.rb @@ -6,7 +6,7 @@ context 'when no memory fields are provided' do it 'returns a function definition with empty properties' do result = described_class.definition([]) - + expect(result).to be_a(Hash) expect(result[:type]).to eq('function') expect(result[:function][:name]).to eq('core_memory_save') @@ -26,7 +26,7 @@ it 'returns a function definition with correct properties' do result = described_class.definition(memory_fields) - + expect(result).to be_a(Hash) expect(result[:type]).to eq('function') expect(result[:function]).to include( @@ -56,11 +56,11 @@ it 'includes all fields in the properties' do result = described_class.definition(memory_fields) properties = result[:function][:parameters][:properties] - + expect(properties.keys).to contain_exactly('name', 'age') expect(properties['name'][:type]).to eq('string') expect(properties['age'][:type]).to eq('string') end end end -end \ No newline at end of file +end diff --git a/spec/lib/ruby-openai-swarm/memories/field_spec.rb b/spec/lib/ruby-openai-swarm/memories/field_spec.rb index 6457353..1cba03a 100644 --- a/spec/lib/ruby-openai-swarm/memories/field_spec.rb +++ b/spec/lib/ruby-openai-swarm/memories/field_spec.rb @@ -54,12 +54,12 @@ it 'correctly parses a valid hash' do # Initialize with valid field to avoid validation error field = described_class.new(field: 'initial') - + field.send(:parse_hash, { field: 'test_field', tool_call_description: 'test description' }) - + expect(field.field).to eq('test_field') expect(field.tool_call_description).to eq('test description') end @@ -73,4 +73,4 @@ expect(field.field).to eq('test_field') end end -end \ No newline at end of file +end diff --git a/spec/lib/ruby-openai-swarm/memory_spec.rb b/spec/lib/ruby-openai-swarm/memory_spec.rb index a773506..7ad84ba 100644 --- a/spec/lib/ruby-openai-swarm/memory_spec.rb +++ b/spec/lib/ruby-openai-swarm/memory_spec.rb @@ -98,4 +98,4 @@ memory.get_memories_data end end -end \ No newline at end of file +end diff --git a/spec/lib/ruby-openai-swarm/util/latest_role_user_message_spec.rb b/spec/lib/ruby-openai-swarm/util/latest_role_user_message_spec.rb index a0da95a..6d73b18 100644 --- a/spec/lib/ruby-openai-swarm/util/latest_role_user_message_spec.rb +++ b/spec/lib/ruby-openai-swarm/util/latest_role_user_message_spec.rb @@ -47,7 +47,7 @@ { "role"=>"user", "content"=>"Tell me the weather in London." - }, + }, ] end From 8fde9648107453e5108502b1e65e58a3acc187d3 Mon Sep 17 00:00:00 2001 From: Grayson Chen Date: Mon, 17 Feb 2025 19:43:54 +0800 Subject: [PATCH 11/11] fix typo --- examples/memory/memory_save.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/memory/memory_save.rb b/examples/memory/memory_save.rb index 0a3e65a..fe2d676 100644 --- a/examples/memory/memory_save.rb +++ b/examples/memory/memory_save.rb @@ -3,7 +3,7 @@ client = OpenAISwarm.new model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o-mini" -memory = OpenAISwarm::Memory.new({ memory_fields: ["language", "grade", "name", "sex"] }) +memory = OpenAISwarm::Memory.new(memory_fields: ["language", "grade", "name", "sex"]) memory.function def get_weather(location:) @@ -69,6 +69,3 @@ def get_news(category:) response2 = client.run(agent: chatbot_agent, messages: messages2, debug: env_debug) puts response2.messages.last['content'] - -# memory.entity_store.data -# binding.pry