IC4T commited on
Commit
4aa221b
1 Parent(s): 6997035
Files changed (2) hide show
  1. constants.py +15 -0
  2. ingest.py +168 -0
constants.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from chromadb.config import Settings
4
+
5
+ load_dotenv()
6
+
7
+ # Define the folder for storing database
8
+ PERSIST_DIRECTORY = os.environ.get('PERSIST_DIRECTORY')
9
+
10
+ # Define the Chroma settings
11
+ CHROMA_SETTINGS = Settings(
12
+ chroma_db_impl='duckdb+parquet',
13
+ persist_directory=PERSIST_DIRECTORY,
14
+ anonymized_telemetry=False
15
+ )
ingest.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import glob
4
+ from typing import List
5
+ from dotenv import load_dotenv
6
+ from multiprocessing import Pool
7
+ from tqdm import tqdm
8
+
9
+ from langchain.document_loaders import (
10
+ CSVLoader,
11
+ EverNoteLoader,
12
+ PDFMinerLoader,
13
+ TextLoader,
14
+ UnstructuredEmailLoader,
15
+ UnstructuredEPubLoader,
16
+ UnstructuredHTMLLoader,
17
+ UnstructuredMarkdownLoader,
18
+ UnstructuredODTLoader,
19
+ UnstructuredPowerPointLoader,
20
+ UnstructuredWordDocumentLoader,
21
+ )
22
+
23
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
24
+ from langchain.vectorstores import Chroma
25
+ from langchain.embeddings import HuggingFaceEmbeddings
26
+ from langchain.docstore.document import Document
27
+ from constants import CHROMA_SETTINGS
28
+
29
+
30
+ load_dotenv()
31
+
32
+
33
+ # Load environment variables
34
+ persist_directory = os.environ.get('PERSIST_DIRECTORY')
35
+ # source_directory = os.environ.get('SOURCE_DIRECTORY', 'source_documents')
36
+ source_directory = os.environ.get('SOURCE_DIRECTORY', 'sample_saham')
37
+
38
+ embeddings_model_name = os.environ.get('EMBEDDINGS_MODEL_NAME')
39
+ chunk_size = 500
40
+ chunk_overlap = 50
41
+
42
+ # Custom document loaders
43
+ class MyElmLoader(UnstructuredEmailLoader):
44
+ """Wrapper to fallback to text/plain when default does not work"""
45
+
46
+ def load(self) -> List[Document]:
47
+ """Wrapper adding fallback for elm without html"""
48
+ try:
49
+ try:
50
+ doc = UnstructuredEmailLoader.load(self)
51
+ except ValueError as e:
52
+ if 'text/html content not found in email' in str(e):
53
+ # Try plain text
54
+ self.unstructured_kwargs["content_source"]="text/plain"
55
+ doc = UnstructuredEmailLoader.load(self)
56
+ else:
57
+ raise
58
+ except Exception as e:
59
+ # Add file_path to exception message
60
+ raise type(e)(f"{self.file_path}: {e}") from e
61
+
62
+ return doc
63
+
64
+
65
+ # Map file extensions to document loaders and their arguments
66
+ LOADER_MAPPING = {
67
+ ".csv": (CSVLoader, {}),
68
+ # ".docx": (Docx2txtLoader, {}),
69
+ ".doc": (UnstructuredWordDocumentLoader, {}),
70
+ ".docx": (UnstructuredWordDocumentLoader, {}),
71
+ ".enex": (EverNoteLoader, {}),
72
+ ".eml": (MyElmLoader, {}),
73
+ ".epub": (UnstructuredEPubLoader, {}),
74
+ ".html": (UnstructuredHTMLLoader, {}),
75
+ ".md": (UnstructuredMarkdownLoader, {}),
76
+ ".odt": (UnstructuredODTLoader, {}),
77
+ ".pdf": (PDFMinerLoader, {}),
78
+ ".ppt": (UnstructuredPowerPointLoader, {}),
79
+ ".pptx": (UnstructuredPowerPointLoader, {}),
80
+ ".txt": (TextLoader, {"encoding": "utf8"}),
81
+ # Add more mappings for other file extensions and loaders as needed
82
+ }
83
+
84
+
85
+ def load_single_document(file_path: str) -> Document:
86
+ ext = "." + file_path.rsplit(".", 1)[-1]
87
+ if ext in LOADER_MAPPING:
88
+ loader_class, loader_args = LOADER_MAPPING[ext]
89
+ loader = loader_class(file_path, **loader_args)
90
+ return loader.load()[0]
91
+
92
+ raise ValueError(f"Unsupported file extension '{ext}'")
93
+
94
+
95
+ def load_documents(source_dir: str, ignored_files: List[str] = []) -> List[Document]:
96
+ """
97
+ Loads all documents from the source documents directory, ignoring specified files
98
+ """
99
+ all_files = []
100
+ for ext in LOADER_MAPPING:
101
+ all_files.extend(
102
+ glob.glob(os.path.join(source_dir, f"**/*{ext}"), recursive=True)
103
+ )
104
+ filtered_files = [file_path for file_path in all_files if file_path not in ignored_files]
105
+
106
+ with Pool(processes=os.cpu_count()) as pool:
107
+ results = []
108
+ with tqdm(total=len(filtered_files), desc='Loading new documents', ncols=80) as pbar:
109
+ for i, doc in enumerate(pool.imap_unordered(load_single_document, filtered_files)):
110
+ results.append(doc)
111
+ pbar.update()
112
+
113
+ return results
114
+
115
+ def process_documents(ignored_files: List[str] = []) -> List[Document]:
116
+ """
117
+ Load documents and split in chunks
118
+ """
119
+ print(f"Loading documents from {source_directory}")
120
+ documents = load_documents(source_directory, ignored_files)
121
+ if not documents:
122
+ print("No new documents to load")
123
+ exit(0)
124
+ print(f"Loaded {len(documents)} new documents from {source_directory}")
125
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
126
+ texts = text_splitter.split_documents(documents)
127
+ print(f"Split into {len(texts)} chunks of text (max. {chunk_size} tokens each)")
128
+ return texts
129
+
130
+ def does_vectorstore_exist(persist_directory: str) -> bool:
131
+ """
132
+ Checks if vectorstore exists
133
+ """
134
+ if os.path.exists(os.path.join(persist_directory, 'index')):
135
+ if os.path.exists(os.path.join(persist_directory, 'chroma-collections.parquet')) and os.path.exists(os.path.join(persist_directory, 'chroma-embeddings.parquet')):
136
+ list_index_files = glob.glob(os.path.join(persist_directory, 'index/*.bin'))
137
+ list_index_files += glob.glob(os.path.join(persist_directory, 'index/*.pkl'))
138
+ # At least 3 documents are needed in a working vectorstore
139
+ if len(list_index_files) > 3:
140
+ return True
141
+ return False
142
+
143
+ def main():
144
+ # Create embeddings
145
+ embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_name)
146
+
147
+ if does_vectorstore_exist(persist_directory):
148
+ # Update and store locally vectorstore
149
+ print(f"Appending to existing vectorstore at {persist_directory}")
150
+ db = Chroma(persist_directory=persist_directory, embedding_function=embeddings, client_settings=CHROMA_SETTINGS)
151
+ collection = db.get()
152
+ texts = process_documents([metadata['source'] for metadata in collection['metadatas']])
153
+ print(f"Creating embeddings. May take some minutes...")
154
+ db.add_documents(texts)
155
+ else:
156
+ # Create and store locally vectorstore
157
+ print("Creating new vectorstore")
158
+ texts = process_documents()
159
+ print(f"Creating embeddings. May take some minutes...")
160
+ db = Chroma.from_documents(texts, embeddings, persist_directory=persist_directory, client_settings=CHROMA_SETTINGS)
161
+ db.persist()
162
+ db = None
163
+
164
+ print(f"Ingestion complete! You can now run QuGPT.py to query your documents")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()