Skip to content
71 changes: 71 additions & 0 deletions examples/memory/memory_save.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require_relative "../bootstrap"

client = OpenAISwarm.new
model = ENV['SWARM_AGENT_DEFAULT_MODEL'] || "gpt-4o-mini"

memory = OpenAISwarm::Memory.new(memory_fields: ["language", "grade", "name", "sex"])
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",
"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
)

messages1 = [
{
"role": "user",
"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."
}
]

puts "first call, set memory"
puts "messages: #{messages1}"

response1 = client.run(agent: chatbot_agent, messages: messages1, debug: env_debug)
puts "memory data: #{memory.entity_store.data}"
puts response1.messages.last['content']

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']
5 changes: 5 additions & 0 deletions lib/ruby-openai-swarm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
require 'ruby-openai-swarm/repl'
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/memories/core_memory_function'
require 'ruby-openai-swarm/memories/field'


module OpenAISwarm
class Error < StandardError;
Expand Down
13 changes: 11 additions & 2 deletions lib/ruby-openai-swarm/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand All @@ -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
Expand All @@ -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
30 changes: 24 additions & 6 deletions lib/ruby-openai-swarm/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,28 @@ def initialize(client = nil)
@logger = OpenAISwarm::Logger.instance.logger
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
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)

Expand Down Expand Up @@ -66,9 +83,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

Expand Down Expand Up @@ -182,11 +200,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
Expand Down
27 changes: 27 additions & 0 deletions lib/ruby-openai-swarm/memories/core_memory_function.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module OpenAISwarm
module Memories
class CoreMemoryFunction
def self.definition(memory_fields = [])
properties = {}

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

{
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,
}
}
}
end
end
end
end
54 changes: 54 additions & 0 deletions lib/ruby-openai-swarm/memories/entity_store.rb
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions lib/ruby-openai-swarm/memories/field.rb
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions lib/ruby-openai-swarm/memory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module OpenAISwarm
class Memory
attr_reader :memories,
:entity_store

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

def prompt_content
return nil if get_memories_data.nil?

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" \
"#{get_memories_data}"
end

def function
return nil if @memory_fields.empty?

OpenAISwarm::FunctionDescriptor.new(
target_method: method(:core_memory_save),
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
end
end
2 changes: 1 addition & 1 deletion lib/ruby-openai-swarm/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module OpenAISwarm
VERSION = "0.4.0.2"
VERSION = "0.5.0"
end
Loading