Using long term memory
This week I finally managed to create a docker image for anyone who wants to use espionox with the long term memory feature. Enabling this feature now takes 5 (relatively) simple steps (skip to Discovering new problems if you don’t care to know how to use the image):
Add
long_term_memory
feature flag to the espionox dependencyCreate a
docker-compose.yaml
file with the following content:version: '3' services: db: image: voidkandy/espionox-image:latest environment: - HOST=${DB_HOST} - PORT=${DB_PORT} - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME} - RESET_DB=true ports: - "${DB_PORT}:5432"
Add needed environment variables to their
.env
Create and fill out the
espionox_config/default.yaml
file with the same values used in their.env
, should look something like this:language_model: api_key: "openai_api_key" database: host: 127.0.0.1 port: 5432 username: "root" password: "super.secure.password" database_name: "espionox"
Run these 2 commands in a terminal window to get the database up and running:
docker-compose build; docker-compose up;
After going through those steps any dev using espionox should be able to experiment with long term memory to their hearts content….once the API is actually finished.
Discovering new problems
After creating the docker image, and adjusting the library for easy setup, I realized there was a lot to be desired from long term memory. Given the fact that I’ve only been working on this library for about 4 months (my initial GitHub commit was on June 24th), I don’t expect any features to be fully fleshed out or working perfectly. But once I started using long term memory for myself, I realized the implementation was seriously flawed. Take a look at how memory was represented before I began to refactor, even reimplement, the memory system:
Context
was the struct that used to handle an agent’s memory. There are more flaws with this implementation than I am willing to go into fully, but the buffer_source
field should give you a good idea of how bad it really is.
Buffer source
Context has a .buffer()
method which returns the current memory buffer based on the value of it’s buffer_source
field. The buffer is just a history of messages between the user and agent. Looks something like this:
[
{
role: “user”,
content: “Hello!“,
},
{
role: "assistant",
content: "Hello! How can I help you?"
},
etc..
]
The buffer used to be stored in an identical data structure in either long or short term memory, and was grabbed from one or the other based on a field value. In this implementation the use of the term `long term memory` is a lie and would be better described as “short term memory that persists across runtimes”.
The reimplementation of memory
I knew I had to rework the entire memory system, but I didn’t know where to start. For inspiration, I turned to the works of people much smarter than myself and came across this paper. The agents in the paper are build with an architecture which outlines the relationship between long and short term memory very differently from how I had. Short term memory has direct access to what would be analogous to my own buffer
, while long term memory contains more abstracted representations of the buffer in the form of summaries, observations, and reflections, each of which are generated by prompting other specialized agents. Short and long term memory then talk to each other when deemed necessary. This lead to my new Memory
struct:
The term ‘ShortTermMemory’ has been completely purged from the project, and is now just called cache. RecallMode
and CachingMechanism
are for memory recall and storage respectively. Because long term memory is behind a feature flag, memory can work with or without the presence of LTM.
As of right now, RecallMode
and CachingMechanism
aren’t very fleshed out, but below is their definitions:
RecallMode
Anytime memory is retrieved from a source, RecallMode
will tell the agent if it needs to run any special functions on, or in preparation of receiving, that memory. As of right now, RecallMode
only has a default variant, which doesn’t actually do anything yet, but structuring memory in this way gives me the opportunity to create different variations of how an agent recalls memory.
CachingMechanism
Anytime memory is stored, the CachingMechanism
of the agent is referenced to determine how to handle the storage of that memory. As of right now, there are two variants of CachingMechanism:
Forgetful - Once the cache exceeds a single back and forth between a user and an agent, it is completely reset. This is useful for agents who’s function require a very controlled context. Such as summarizers, who should only receive content to summarize and then respond to that.
SummarizeAtLimit - This gives users control over a limit to set for an agents cache, and a flag for whether or not the cache should be saved to LTM after being summarized. Once the given cache size limit is reached, the entire message cache is fed to a summarizer agent, the cache is then cleared, and the agent receives the summary of the cache before it was cleared. Allowing it to remember the conversation without having a massive cache.
The future
This week I built a custom docker image and reworked the memory system of espionox. There are still a few kinks to smooth over for the image but it is mostly finished. The memory system has been written to be changed, and this week I intend on getting my GUI up to date with the most recent changes and experimenting with more CachingMechanisms and RecallModes. I also need to create a sandbox environment for agents to interface with to give them the ability to communicate with each other, write and execute code, browse the web, or anything else I might want them to do. I’ve began reading a lot of papers that explore how to create effective agents and I’m excited to try to create my own implementations of the architectures mentioned in those papers.