StudyLLM / rag.py
Swaroop Vedula
Upload 2 files
45a3b62 verified
Raw
History Blame Contribute Delete
3.1 kB
"""
RAG support: chunking extracted text, indexing it into Chroma, and
retrieving relevant chunks for a given query.
"""
import chromadb
import re
def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 100) -> list[str]:
"""
Split raw text into overlapping chunks.
"""
start = 0
chunks = []
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunks
def build_collection(chunks: list[str]):
"""
Embed and index the given chunks into a fresh Chroma collection.
"""
if not chunks:
return None
client = chromadb.Client()
try:
client.delete_collection("study_material")
except Exception:
pass
collection = client.create_collection("study_material")
collection.add(documents=chunks, ids=[f"chunk_{i}" for i in range(len(chunks))])
return collection
def retrieve_relevant_chunks(collection, query: str, k: int = 4) -> list[str]:
"""
Given a user's question, return the top-k most relevant chunks.
"""
if collection is None: return []
num_docs = collection.count()
if num_docs == 0:
return []
actual_k = min(k, num_docs)
results = collection.query(query_texts=[query], n_results=actual_k)
return results["documents"][0]
def find_direct_reference(study_context: str, query: str) -> str:
"""ch
If the query references a specific numbered item (e.g. "question 5",
"problem 12", "chapter 3"), search the raw text directly for that label
and return a window of text starting there. This catches literal
lookups that semantic embedding search is bad at.
Returns an empty string if no reference pattern is found in the query,
or if that label doesn't appear anywhere in study_context.
"""
match = re.search(r'\b(question|problem|exercise|chapter)\s*#?\s*(\d+)\b', query, re.IGNORECASE)
if not match:
return ""
label, number = match.group(1), match.group(2)
# Look for that exact label + number in the source text
pattern = re.compile(rf'\b{label}\s*#?\s*{number}\b', re.IGNORECASE)
found = pattern.search(study_context)
if not found:
return ""
# Grab a chunk of text starting at the match — enough to likely contain
# the full question, without running all the way to the end of the doc.
start = found.start()
return study_context[start:start + 2000]
def clean_math_output(text: str) -> str:
# \[ ... \] -> $$ ... $$ , \( ... \) -> $ ... $
text = re.sub(r'\\\[(.*?)\\\]', r'$$\1$$', text, flags=re.DOTALL)
text = re.sub(r'\\\((.*?)\\\)', r'$\1$', text, flags=re.DOTALL)
# Catches cases where the model skips delimiters entirely and just uses
# plain ( ) around LaTeX-flavored content.
text = re.sub(r'\(([^()]*\\[a-zA-Z]+[^()]*)\)', r'$\1$', text)
# Strip cosmetic sizing commands
text = re.sub(r'\\[Bb]igl|\\[Bb]igr|\\[Bb]ig\b', '', text)
# Fix stray semicolons before \text{}
text = re.sub(r';(\\text)', r'\\,\1', text)
return text