From 8a14066e39c0882329451d70e5eafea6d05cb98f Mon Sep 17 00:00:00 2001 From: Desmond Grealy Date: Fri, 2 Jun 2023 12:52:37 -0700 Subject: [PATCH 1/2] Class to facilitate embedding generated documents to chromadb --- .gitignore | 4 ++ constants.py | 5 +++ src/__init__.py | 0 src/chroma-embeddings.parquet | Bin 0 -> 10971 bytes src/embeddings.py | 71 ++++++++++++++++++++++++++++++++++ src/traversal.py | 23 +++++++++++ 6 files changed, 103 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/chroma-embeddings.parquet create mode 100644 src/embeddings.py create mode 100644 src/traversal.py diff --git a/.gitignore b/.gitignore index 826fa7e54..5a9ae1b80 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,9 @@ __pycache__ # Ignore everything in the generated directory /generated/* +# Ignore the locally persisted chromadb +.chroma_db/ + # Don't ignore .gitkeep files in the generated directory !/generated/.gitkeep + diff --git a/constants.py b/constants.py index b7ccc11b4..9eaa2b8f8 100644 --- a/constants.py +++ b/constants.py @@ -1,4 +1,9 @@ +import os + + EXTENSION_TO_SKIP = [".png",".jpg",".jpeg",".gif",".bmp",".svg",".ico",".tif",".tiff"] DEFAULT_DIR = "generated" +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_DIR_FULL_PATH = os.path.join(ROOT_DIR, DEFAULT_DIR) DEFAULT_MODEL = "gpt-3.5-turbo" # we recommend 'gpt-4' if you have it # gpt3.5 is going to be worse at generating code so we strongly recommend gpt4. i know most people dont have access, we are working on a hosted version DEFAULT_MAX_TOKENS = 2000 # i wonder how to tweak this properly. we dont want it to be max length as it encourages verbosity of code. but too short and code also truncates suddenly. \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/chroma-embeddings.parquet b/src/chroma-embeddings.parquet new file mode 100644 index 0000000000000000000000000000000000000000..6b8551884f59aea7db3b5542940c2379cd396a41 GIT binary patch literal 10971 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 literal 0 HcmV?d00001 diff --git a/src/embeddings.py b/src/embeddings.py new file mode 100644 index 000000000..440774ac1 --- /dev/null +++ b/src/embeddings.py @@ -0,0 +1,71 @@ +import os + +from termcolor import colored +import chromadb + +from constants import ROOT_DIR +from src.traversal import traverse_dir + + +class Embeddings(): + + EXCLUDE_PATTERNS = ["*.png","*.jpg","*.jpeg","*.gif","*.bmp","*.svg","*.ico","*.tif","*.tiff"] + + def __init__(self, debug=False): + self.debug = debug + self.GENERATED_FILES_COLLECTION_NAME = "generated_files" + + DB_DIR = os.path.join(ROOT_DIR, ".chroma_db") + # Create the embedding database directory if it doesn't exist + if not os.path.exists(DB_DIR): + os.makedirs(DB_DIR) + + self.client = chromadb.Client( + chromadb.config.Settings( + chroma_db_impl="duckdb+parquet", + persist_directory=DB_DIR, + ) + ) + + self.ensure_generated_files_collection_exists() + + def ensure_generated_files_collection_exists(self): + # Create the generated files collection if it doesn't exist + self.generated_files_collection = self.client.get_or_create_collection(self.GENERATED_FILES_COLLECTION_NAME) + + + def persist_generated_file_contents(self, reset=False): + if reset: + self.generated_files_collection.reset() + self.ensure_generated_files_collection_exists() + + # Iterate over all files in the generated directory + file_paths_list = [] + file_contents_list = [] + metadatas_list = [] + for file_path in traverse_dir("generated", exclude_patterns=self.EXCLUDE_PATTERNS): + file_paths_list.append(file_path) + if self.debug: + print("embedding: " + colored(file_path, 'green')) + # Read the file + with open(file_path, "r") as file: + file_contents_list.append(file.read()) + # Get the filename + filename = os.path.basename(file_path) + # Get the extension + extension = filename.split(".")[-1] + metadatas_list.append({ + "filename": filename, + "extension": extension, + }) + + # Upsert the file into the database + self.generated_files_collection.upsert( + documents=file_contents_list, + metadatas=metadatas_list, + ids=file_paths_list, + ) + if self.debug: + print(colored("persisted embeddings for %s files." % len(file_paths_list), 'yellow')) + + \ No newline at end of file diff --git a/src/traversal.py b/src/traversal.py new file mode 100644 index 000000000..67dfd9e82 --- /dev/null +++ b/src/traversal.py @@ -0,0 +1,23 @@ +import os +import fnmatch + +def traverse_dir(root_dir, include_patterns=None, exclude_patterns=None): + """ + # Example usage: + root_dir = '/path/to/directory' + include_patterns = ['*.txt', '*.py'] # Include files matching these patterns + exclude_patterns = ['exclude_dir1/*', 'exclude_dir2/*'] # Exclude directories matching these patterns + + for file_path in traverse_dir(root_dir, include_patterns=include_patterns, exclude_patterns=exclude_patterns): + print(file_path) + """ + for root, dirs, files in os.walk(root_dir): + if exclude_patterns and any(fnmatch.fnmatch(root, pattern) for pattern in exclude_patterns): + continue + for file in files: + file_path = os.path.join(root, file) + if include_patterns and not any(fnmatch.fnmatch(file_path, pattern) for pattern in include_patterns): + continue + yield file_path + + From 339cb632513aa45c3b23ccec87d04a6c574d9a6f Mon Sep 17 00:00:00 2001 From: Desmond Grealy Date: Fri, 2 Jun 2023 13:22:06 -0700 Subject: [PATCH 2/2] Add chromadb artifact to .gitignore --- .gitignore | 1 + src/chroma-embeddings.parquet | Bin 10971 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 src/chroma-embeddings.parquet diff --git a/.gitignore b/.gitignore index 5a9ae1b80..c928174cb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__ # Ignore the locally persisted chromadb .chroma_db/ +**/chroma-embeddings.parquet # Don't ignore .gitkeep files in the generated directory !/generated/.gitkeep diff --git a/src/chroma-embeddings.parquet b/src/chroma-embeddings.parquet deleted file mode 100644 index 6b8551884f59aea7db3b5542940c2379cd396a41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10971 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