Rishit Pant commited on
Commit
a91b42a
·
1 Parent(s): 5976a7b

Chroma cloud setup (#11)

Browse files
Files changed (3) hide show
  1. src/ingest.py +35 -39
  2. src/retrieve.py +27 -4
  3. src/tracing.py +2 -3
src/ingest.py CHANGED
@@ -1,10 +1,14 @@
 
1
  import re
2
  from pathlib import Path
 
3
  from langchain_core.documents import Document
4
  from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
5
  from langchain_chroma import Chroma
6
  from langchain_huggingface import HuggingFaceEmbeddings
7
 
 
 
8
  DATA_DIR = Path("data")
9
  DB_DIR = "db"
10
  COLLECTION_NAME = "handbook_docs"
@@ -13,6 +17,25 @@ EMBED_MODEL = "BAAI/bge-small-en-v1.5"
13
  CHUNK_SIZE = 2000
14
  CHUNK_OVERLAP = 200
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  HEADERS_TO_SPLIT = [
17
  ("#", "h1"),
18
  ("##", "h2"),
@@ -49,37 +72,15 @@ def html_tables_to_markdown(text: str) -> str:
49
  def clean_markdown(raw_text: str) -> str:
50
  noise_patterns = [
51
  r'\d{1,2}/\d{1,2}/\d{2,4},\s+\d{1,2}:\d{2}\s+[AP]M[^\n]*',
52
-
53
- # Full footer form: "Google Docs Published using Google Docs"
54
  r'Google Docs(?:\s+icon|\s+logo)?\s+Published using Google Docs[^\n]*',
55
-
56
- # Fragment form that appears mid-chunk after splitting:
57
- # " 10/66 info icon Published using Google Docs IITM BS Degree ..."
58
  r'\d+/\d+\s+info\s+icon\s+Published\s+using\s+Google\s+Docs[^\n]*',
59
-
60
- # Standalone "Published using Google Docs" anywhere
61
  r'Published\s+using\s+Google\s+Docs[^\n]*',
62
-
63
- # "info icon" artifact on its own or leading a sentence
64
  r'\binfo\s+icon\b[^\n]*',
65
-
66
- # Document title trailers: "IITM BS Degree Programme - Student Handb..."
67
- # These appear as orphaned suffixes after the noise strip above
68
  r'IITM\s+BS\s+Degree\s+Programme\s*[-–]\s*Student\s+Hand\w*[^\n]*',
69
-
70
- # Report abuse / learn more footer
71
  r'Report\s+abuse\s+Learn\s+more[^\n]*',
72
-
73
- # Auto-update notice
74
  r'Updated\s+automatically\s+every\s+\d+\s+minutes[^\n]*',
75
-
76
- # Google Docs URLs
77
  r'https://docs\.google\.com/\S+',
78
-
79
- # Bare page-number artifacts: " 38/66 " or "10/66" on their own line
80
- # (excludes /100 since that's always a score threshold, e.g. "40/100")
81
  r'(?<![\d>=])\b\d{1,3}/(?!100\b)\d{2,3}\b(?!\d)\s*(?=\n|$)',
82
-
83
  r'^#{1,3}\s*BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
84
  r'^BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
85
  r'^Updated\s+automatically\s+every\s+\d+\s+minutes\s*$',
@@ -89,9 +90,8 @@ def clean_markdown(raw_text: str) -> str:
89
  text = re.sub(pattern, '', text, flags=re.IGNORECASE | re.MULTILINE)
90
 
91
  text = html_tables_to_markdown(text)
92
- # Collapse runs of whitespace left behind by removals
93
- text = re.sub(r'[ \t]{2,}', ' ', text) # multiple spaces → single
94
- text = re.sub(r'\n{3,}', '\n\n', text).strip() # blank line runs → one
95
  return text
96
 
97
 
@@ -101,7 +101,7 @@ def load_and_split(md_path: Path) -> list[Document]:
101
 
102
  header_splitter = MarkdownHeaderTextSplitter(
103
  headers_to_split_on=HEADERS_TO_SPLIT,
104
- strip_headers=False,
105
  return_each_line=False,
106
  )
107
  header_docs = header_splitter.split_text(cleaned)
@@ -113,11 +113,6 @@ def load_and_split(md_path: Path) -> list[Document]:
113
  )
114
  final_docs = char_splitter.split_documents(header_docs)
115
 
116
- # Prepend the section's header path to every chunk's content.
117
- # Without this, a chunk containing e.g. a course's T formula but not
118
- # the course name (because the header line landed in an earlier chunk)
119
- # won't match queries that mention the course name — for either the
120
- # vector embedding or BM25.
121
  for doc in final_docs:
122
  header_parts = [
123
  doc.metadata.get("h1", ""),
@@ -127,11 +122,10 @@ def load_and_split(md_path: Path) -> list[Document]:
127
  header_path = " > ".join(p for p in header_parts if p)
128
  if header_path:
129
  doc.page_content = (
130
- f"[Course: {header_path}]\n"
131
- f"{doc.page_content}\n"
132
- f"[/Course: {header_path}]"
133
- )
134
-
135
 
136
  for doc in final_docs:
137
  doc.metadata["source"] = md_path.name
@@ -155,13 +149,15 @@ def build_index():
155
  if not all_docs:
156
  print("No valid documents found. Exiting.")
157
  return
158
-
159
- print(f"\nEmbedding and indexing {len(all_docs)} chunks into ChromaDB...")
160
 
 
 
 
 
161
  vectorstore = Chroma.from_documents(
162
  documents=all_docs,
163
  embedding=embeddings,
164
- persist_directory=DB_DIR,
165
  collection_name=COLLECTION_NAME,
166
  )
167
 
 
1
+ import os
2
  import re
3
  from pathlib import Path
4
+ from dotenv import load_dotenv
5
  from langchain_core.documents import Document
6
  from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
7
  from langchain_chroma import Chroma
8
  from langchain_huggingface import HuggingFaceEmbeddings
9
 
10
+ load_dotenv()
11
+
12
  DATA_DIR = Path("data")
13
  DB_DIR = "db"
14
  COLLECTION_NAME = "handbook_docs"
 
17
  CHUNK_SIZE = 2000
18
  CHUNK_OVERLAP = 200
19
 
20
+ CHROMA_API_KEY = os.getenv("CHROMA_API_KEY")
21
+ CHROMA_TENANT = os.getenv("CHROMA_TENANT")
22
+ CHROMA_DATABASE = os.getenv("CHROMA_DATABASE")
23
+ USE_CHROMA_CLOUD = bool(CHROMA_API_KEY)
24
+
25
+
26
+ def get_chroma_client():
27
+ """Return a chromadb client — CloudClient if Chroma Cloud env vars are
28
+ set, otherwise a local PersistentClient writing to DB_DIR."""
29
+ import chromadb
30
+
31
+ if USE_CHROMA_CLOUD:
32
+ return chromadb.CloudClient(
33
+ api_key=CHROMA_API_KEY,
34
+ tenant=CHROMA_TENANT,
35
+ database=CHROMA_DATABASE,
36
+ )
37
+ return chromadb.PersistentClient(path=DB_DIR)
38
+
39
  HEADERS_TO_SPLIT = [
40
  ("#", "h1"),
41
  ("##", "h2"),
 
72
  def clean_markdown(raw_text: str) -> str:
73
  noise_patterns = [
74
  r'\d{1,2}/\d{1,2}/\d{2,4},\s+\d{1,2}:\d{2}\s+[AP]M[^\n]*',
 
 
75
  r'Google Docs(?:\s+icon|\s+logo)?\s+Published using Google Docs[^\n]*',
 
 
 
76
  r'\d+/\d+\s+info\s+icon\s+Published\s+using\s+Google\s+Docs[^\n]*',
 
 
77
  r'Published\s+using\s+Google\s+Docs[^\n]*',
 
 
78
  r'\binfo\s+icon\b[^\n]*',
 
 
 
79
  r'IITM\s+BS\s+Degree\s+Programme\s*[-–]\s*Student\s+Hand\w*[^\n]*',
 
 
80
  r'Report\s+abuse\s+Learn\s+more[^\n]*',
 
 
81
  r'Updated\s+automatically\s+every\s+\d+\s+minutes[^\n]*',
 
 
82
  r'https://docs\.google\.com/\S+',
 
 
 
83
  r'(?<![\d>=])\b\d{1,3}/(?!100\b)\d{2,3}\b(?!\d)\s*(?=\n|$)',
 
84
  r'^#{1,3}\s*BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
85
  r'^BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
86
  r'^Updated\s+automatically\s+every\s+\d+\s+minutes\s*$',
 
90
  text = re.sub(pattern, '', text, flags=re.IGNORECASE | re.MULTILINE)
91
 
92
  text = html_tables_to_markdown(text)
93
+ text = re.sub(r'[ \t]{2,}', ' ', text)
94
+ text = re.sub(r'\n{3,}', '\n\n', text).strip()
 
95
  return text
96
 
97
 
 
101
 
102
  header_splitter = MarkdownHeaderTextSplitter(
103
  headers_to_split_on=HEADERS_TO_SPLIT,
104
+ strip_headers=False,
105
  return_each_line=False,
106
  )
107
  header_docs = header_splitter.split_text(cleaned)
 
113
  )
114
  final_docs = char_splitter.split_documents(header_docs)
115
 
 
 
 
 
 
116
  for doc in final_docs:
117
  header_parts = [
118
  doc.metadata.get("h1", ""),
 
122
  header_path = " > ".join(p for p in header_parts if p)
123
  if header_path:
124
  doc.page_content = (
125
+ f"[Course: {header_path}]\n"
126
+ f"{doc.page_content}\n"
127
+ f"[/Course: {header_path}]"
128
+ )
 
129
 
130
  for doc in final_docs:
131
  doc.metadata["source"] = md_path.name
 
149
  if not all_docs:
150
  print("No valid documents found. Exiting.")
151
  return
 
 
152
 
153
+ target = "Chroma Cloud" if USE_CHROMA_CLOUD else f"local dir '{DB_DIR}'"
154
+ print(f"\nEmbedding and indexing {len(all_docs)} chunks into {target}...")
155
+
156
+ client = get_chroma_client()
157
  vectorstore = Chroma.from_documents(
158
  documents=all_docs,
159
  embedding=embeddings,
160
+ client=client,
161
  collection_name=COLLECTION_NAME,
162
  )
163
 
src/retrieve.py CHANGED
@@ -1,4 +1,7 @@
1
  import re
 
 
 
2
  from langchain_chroma import Chroma
3
  from langchain_huggingface import HuggingFaceEmbeddings
4
  from langchain_community.retrievers import BM25Retriever
@@ -6,6 +9,8 @@ from langchain_community.cross_encoders import HuggingFaceCrossEncoder
6
  from langchain_core.documents import Document
7
  from langchain_classic.retrievers import EnsembleRetriever
8
 
 
 
9
  DB_DIR = "db"
10
  COLLECTION_NAME = "handbook_docs"
11
  EMBED_MODEL = "BAAI/bge-small-en-v1.5"
@@ -17,6 +22,21 @@ FINAL_TOP_N = 5 # chunks returned after reranking
17
  # Weights for RRF fusion: [vector_weight, bm25_weight]
18
  ENSEMBLE_WEIGHTS = [0.6, 0.4]
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  _NOISE_PATTERNS = [
22
  re.compile(r'\d+/\d+\s+info\s+icon\s+Published\s+using\s+Google\s+Docs[^\n]*', re.I),
@@ -47,15 +67,18 @@ def _is_toc_chunk(text: str) -> bool:
47
 
48
  class Retriever:
49
  def __init__(self):
 
 
 
50
  embeddings = HuggingFaceEmbeddings(
51
  model_name=EMBED_MODEL,
52
  encode_kwargs={"normalize_embeddings": True},
53
  )
54
 
55
  vectorstore = Chroma(
 
56
  collection_name=COLLECTION_NAME,
57
  embedding_function=embeddings,
58
- persist_directory=DB_DIR,
59
  )
60
  vector_retriever = vectorstore.as_retriever(
61
  search_type="similarity",
@@ -122,9 +145,9 @@ if __name__ == "__main__":
122
  r = Retriever()
123
 
124
  queries = [
125
- "What is the revised fee for the foundation level?",
126
- "Eligibility to appear for the qualifier exam?",
127
- "When is the End Term Exam for May 2026?",
128
  ]
129
 
130
  for q in queries:
 
1
  import re
2
+ from dotenv import load_dotenv
3
+ import os
4
+ import chromadb
5
  from langchain_chroma import Chroma
6
  from langchain_huggingface import HuggingFaceEmbeddings
7
  from langchain_community.retrievers import BM25Retriever
 
9
  from langchain_core.documents import Document
10
  from langchain_classic.retrievers import EnsembleRetriever
11
 
12
+ load_dotenv()
13
+
14
  DB_DIR = "db"
15
  COLLECTION_NAME = "handbook_docs"
16
  EMBED_MODEL = "BAAI/bge-small-en-v1.5"
 
22
  # Weights for RRF fusion: [vector_weight, bm25_weight]
23
  ENSEMBLE_WEIGHTS = [0.6, 0.4]
24
 
25
+ CHROMA_API_KEY = os.getenv("CHROMA_API_KEY")
26
+ CHROMA_TENANT = os.getenv("CHROMA_TENANT")
27
+ CHROMA_DATABASE = os.getenv("CHROMA_DATABASE")
28
+ USE_CHROMA_CLOUD = bool(CHROMA_API_KEY)
29
+
30
+
31
+ def get_chroma_client():
32
+ if USE_CHROMA_CLOUD:
33
+ return chromadb.CloudClient(
34
+ api_key=CHROMA_API_KEY,
35
+ tenant=CHROMA_TENANT,
36
+ database=CHROMA_DATABASE,
37
+ )
38
+ return chromadb.PersistentClient(path=DB_DIR)
39
+
40
 
41
  _NOISE_PATTERNS = [
42
  re.compile(r'\d+/\d+\s+info\s+icon\s+Published\s+using\s+Google\s+Docs[^\n]*', re.I),
 
67
 
68
  class Retriever:
69
  def __init__(self):
70
+ backend = "Chroma Cloud" if USE_CHROMA_CLOUD else f"local dir '{DB_DIR}'"
71
+ print(f"Connecting to Chroma backend: {backend}")
72
+
73
  embeddings = HuggingFaceEmbeddings(
74
  model_name=EMBED_MODEL,
75
  encode_kwargs={"normalize_embeddings": True},
76
  )
77
 
78
  vectorstore = Chroma(
79
+ client=get_chroma_client(),
80
  collection_name=COLLECTION_NAME,
81
  embedding_function=embeddings,
 
82
  )
83
  vector_retriever = vectorstore.as_retriever(
84
  search_type="similarity",
 
145
  r = Retriever()
146
 
147
  queries = [
148
+ "What is the alumni fees?",
149
+ "What is the criteria to pass DBMS OPPE?",
150
+ "What is the end term exam date?",
151
  ]
152
 
153
  for q in queries:
src/tracing.py CHANGED
@@ -1,5 +1,7 @@
1
  import os
2
  from dotenv import load_dotenv
 
 
3
 
4
  load_dotenv()
5
 
@@ -12,9 +14,6 @@ langfuse_handler = None
12
  observe = None
13
 
14
  if LANGFUSE_ENABLED:
15
- from langfuse import get_client, observe as _observe
16
- from langfuse.langchain import CallbackHandler
17
-
18
  langfuse = get_client()
19
  langfuse_handler = CallbackHandler()
20
  observe = _observe
 
1
  import os
2
  from dotenv import load_dotenv
3
+ from langfuse import get_client, observe as _observe
4
+ from langfuse.langchain import CallbackHandler
5
 
6
  load_dotenv()
7
 
 
14
  observe = None
15
 
16
  if LANGFUSE_ENABLED:
 
 
 
17
  langfuse = get_client()
18
  langfuse_handler = CallbackHandler()
19
  observe = _observe