Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Local Cache and Redis Memory backend #372

Merged
merged 18 commits into from
Apr 9, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ export CUSTOM_SEARCH_ENGINE_ID="YOUR_CUSTOM_SEARCH_ENGINE_ID"

```

## Redis Setup

Install docker desktop.

Run:
```
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
```

Set the following environment variables:
```
MEMORY_BACKEND=redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
```

Note that this is not intended to be run facing the internet and is not secure, do not expose redis to the internet without a password or at all really.

You can optionally set

```
WIPE_REDIS_ON_START=False
```

To persist memory stored in Redis.

You can specify the memory index for redis using the following:

````
MEMORY_INDEX=whatever
````

## 🌲 Pinecone API Key Setup

Pinecone enable a vector based memory so a vast memory can be stored and only relevant memories
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ docker
duckduckgo-search
google-api-python-client #(https://developers.google.com/custom-search/v1/overview)
pinecone-client==2.2.1
redis
orjson
Pillow
7 changes: 4 additions & 3 deletions scripts/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import browse
import json
from memory import PineconeMemory
from memory import get_memory
import datetime
import agent_manager as agents
import speak
Expand Down Expand Up @@ -53,10 +53,11 @@ def get_command(response):


def execute_command(command_name, arguments):
memory = PineconeMemory()
memory = get_memory(cfg)

try:
if command_name == "google":

# Check if the Google API key is set and use the official search method
# If the API key is not set or has only whitespaces, use the unofficial search method
if cfg.google_api_key and (cfg.google_api_key.strip() if cfg.google_api_key else None):
Expand Down
16 changes: 14 additions & 2 deletions scripts/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import abc
import os
import openai
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()


class Singleton(type):
class Singleton(abc.ABCMeta, type):
"""
Singleton metaclass for ensuring only one instance of a class.
"""
Expand All @@ -20,6 +21,10 @@ def __call__(cls, *args, **kwargs):
return cls._instances[cls]


class AbstractSingleton(abc.ABC, metaclass=Singleton):
pass


class Config(metaclass=Singleton):
"""
Configuration class to store the state of bools for different scripts access.
Expand Down Expand Up @@ -59,7 +64,14 @@ def __init__(self):
# User agent headers to use when browsing web
# Some websites might just completely deny request with an error code if no user agent was found.
self.user_agent_header = {"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"}

self.redis_host = os.getenv("REDIS_HOST", "localhost")
self.redis_port = os.getenv("REDIS_PORT", "6379")
self.redis_password = os.getenv("REDIS_PASSWORD", "")
self.wipe_redis_on_start = os.getenv("WIPE_REDIS_ON_START", "True") == 'True'
self.memory_index = os.getenv("MEMORY_INDEX", 'auto-gpt')
# Note that indexes must be created on db 0 in redis, this is not configureable.

self.memory_backend = os.getenv("MEMORY_BACKEND", 'local')
# Initialize the OpenAI API client
openai.api_key = self.openai_api_key

Expand Down
7 changes: 2 additions & 5 deletions scripts/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import random
import commands as cmd
from memory import PineconeMemory
from memory import get_memory
import data
import chat
from colorama import Fore, Style
Expand Down Expand Up @@ -281,12 +281,9 @@ def parse_arguments():
# Make a constant:
user_input = "Determine which next command to use, and respond using the format specified above:"

# raise an exception if pinecone_api_key or region is not provided
if not cfg.pinecone_api_key or not cfg.pinecone_region: raise Exception("Please provide pinecone_api_key and pinecone_region")
# Initialize memory and make sure it is empty.
# this is particularly important for indexing and referencing pinecone memory
memory = PineconeMemory()
memory.clear()
memory = get_memory(cfg, init=True)
print('Using memory of type: ' + memory.__class__.__name__)

# Interaction Loop
Expand Down
44 changes: 44 additions & 0 deletions scripts/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from memory.local import LocalCache
try:
from memory.redismem import RedisMemory
except ImportError:
print("Redis not installed. Skipping import.")
RedisMemory = None

try:
from memory.pinecone import PineconeMemory
except ImportError:
print("Pinecone not installed. Skipping import.")
PineconeMemory = None


def get_memory(cfg, init=False):
memory = None
if cfg.memory_backend == "pinecone":
if not PineconeMemory:
print("Error: Pinecone is not installed. Please install pinecone"
" to use Pinecone as a memory backend.")
else:
memory = PineconeMemory(cfg)
if init:
memory.clear()
elif cfg.memory_backend == "redis":
if not RedisMemory:
print("Error: Redis is not installed. Please install redis-py to"
" use Redis as a memory backend.")
else:
memory = RedisMemory(cfg)

if memory is None:
memory = LocalCache(cfg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We appear to be missing a call to memory.clear() here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the LocalCache

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there was a lot of people that wanted the memory to persist by default and I just made Pi's implementation work.

I guess a direction one way or the other needs to be picked.

Deleting the memory is easy enough for local, use just delete the json file.

But there's also a memory index if you wanted to start fresh but keep memory from a different session.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, okay.

Perhaps default persistent memory is a good idea in the near future, however now I think it has the side effect of encouraging the AI to repeat it's errors.

Through my experimenting with this PR, if the AI is reminded of events from it's past were it didn't respond in JSON, it's more likely to do so again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be prudent to not even save those in memory.

Torantulino marked this conversation as resolved.
Show resolved Hide resolved
if init:
memory.clear()
return memory


__all__ = [
"get_memory",
"LocalCache",
"RedisMemory",
"PineconeMemory",
]
31 changes: 31 additions & 0 deletions scripts/memory/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Base class for memory providers."""
import abc
from config import AbstractSingleton
import openai


def get_ada_embedding(text):
text = text.replace("\n", " ")
return openai.Embedding.create(input=[text], model="text-embedding-ada-002")["data"][0]["embedding"]


class MemoryProviderSingleton(AbstractSingleton):
@abc.abstractmethod
def add(self, data):
pass

@abc.abstractmethod
def get(self, data):
pass

@abc.abstractmethod
def clear(self):
pass

@abc.abstractmethod
def get_relevant(self, data, num_relevant=5):
pass

@abc.abstractmethod
def get_stats(self):
pass
111 changes: 111 additions & 0 deletions scripts/memory/local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import dataclasses
import orjson
from typing import Any, List, Optional
import numpy as np
import os
from memory.base import MemoryProviderSingleton, get_ada_embedding


EMBED_DIM = 1536
SAVE_OPTIONS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_DATACLASS


def create_default_embeddings():
return np.zeros((0, EMBED_DIM)).astype(np.float32)


@dataclasses.dataclass
class CacheContent:
texts: List[str] = dataclasses.field(default_factory=list)
embeddings: np.ndarray = dataclasses.field(
default_factory=create_default_embeddings
)


class LocalCache(MemoryProviderSingleton):

# on load, load our database
def __init__(self, cfg) -> None:
self.filename = f"{cfg.memory_index}.json"
if os.path.exists(self.filename):
with open(self.filename, 'rb') as f:
loaded = orjson.loads(f.read())
self.data = CacheContent(**loaded)
else:
self.data = CacheContent()

def add(self, text: str):
"""
Add text to our list of texts, add embedding as row to our
embeddings-matrix

Args:
text: str

Returns: None
"""
self.data.texts.append(text)

embedding = get_ada_embedding(text)

vector = np.array(embedding).astype(np.float32)
vector = vector[np.newaxis, :]
self.data.embeddings = np.concatenate(
[
vector,
self.data.embeddings,
],
axis=0,
)

with open(self.filename, 'wb') as f:
out = orjson.dumps(
self.data,
option=SAVE_OPTIONS
)
f.write(out)

def clear(self) -> str:
"""
Clears the redis server.

Returns: A message indicating that the memory has been cleared.
"""
self.data = CacheContent()
return "Obliviated"

def get(self, data: str) -> Optional[List[Any]]:
"""
Gets the data from the memory that is most relevant to the given data.

Args:
data: The data to compare to.

Returns: The most relevant data.
"""
return self.get_relevant(data, 1)

def get_relevant(self, text: str, k: int) -> List[Any]:
""""
matrix-vector mult to find score-for-each-row-of-matrix
get indices for top-k winning scores
return texts for those indices
Args:
text: str
k: int

Returns: List[str]
"""
embedding = get_ada_embedding(text)

scores = np.dot(self.data.embeddings, embedding)

top_k_indices = np.argsort(scores)[-k:][::-1]

return [self.data.texts[i] for i in top_k_indices]

def get_stats(self):
"""
Returns: The stats of the local cache.
"""
return len(self.data.texts), self.data.embeddings.shape
18 changes: 4 additions & 14 deletions scripts/memory.py → scripts/memory/pinecone.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
from config import Config, Singleton
import pinecone
import openai

cfg = Config()


def get_ada_embedding(text):
text = text.replace("\n", " ")
return openai.Embedding.create(input=[text], model="text-embedding-ada-002")["data"][0]["embedding"]

import pinecone

def get_text_from_embedding(embedding):
return openai.Embedding.retrieve(embedding, model="text-embedding-ada-002")["data"][0]["text"]
from memory.base import MemoryProviderSingleton, get_ada_embedding


class PineconeMemory(metaclass=Singleton):
def __init__(self):
class PineconeMemory(MemoryProviderSingleton):
def __init__(self, cfg):
pinecone_api_key = cfg.pinecone_api_key
pinecone_region = cfg.pinecone_region
pinecone.init(api_key=pinecone_api_key, environment=pinecone_region)
Expand Down
Loading