Skip to content

Commit

Permalink
Merge pull request #372 from BillSchumacher/redis-backend
Browse files Browse the repository at this point in the history
Implement Local Cache and Redis Memory backend. 
Removes dependence on Pinecone
  • Loading branch information
Torantulino authored Apr 9, 2023
2 parents 1ea9c36 + a861dec commit df7ddd4
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 24 deletions.
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)
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
114 changes: 114 additions & 0 deletions scripts/memory/local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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
"""
if 'Command Error:' in text:
return ""
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)
return text

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

0 comments on commit df7ddd4

Please sign in to comment.