hackathon / ingest_train_data.py
Quincy Hsieh
Change endpoint to APIM endpoint
5f5e6b2
Raw
History Blame Contribute Delete
6.84 kB
"""
Ingest Training Data Script
============================
Standalone script to convert all documents in ./train_data into embeddings
and store them in the ChromaDB vector store.
Usage:
python ingest_train_data.py
Requires:
- AZURE_API_KEY environment variable set
- data/config.json (or root config.json as fallback) with endpoint URLs
- Documents (PDF/TXT) in ./train_data/<category>/
"""
import os
import sys
import json
import logging
import time
from pathlib import Path
os.environ.setdefault("ANONYMIZED_TELEMETRY", "False")
import requests as http_requests
import chromadb
from chromadb.config import Settings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pypdf import PdfReader
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).parent
DATA_DIR = Path("/data") if Path("/data").is_dir() else PROJECT_ROOT / "data"
TRAIN_DATA_DIR = Path("/train_data") if Path("/train_data").is_dir() else PROJECT_ROOT / "train_data"
CHROMA_PERSIST_DIR = str(DATA_DIR / "chroma_db")
COLLECTION_NAME = "rag_documents"
CHUNK_SIZE = 512
CHUNK_OVERLAP = 50
EMBEDDING_BATCH_SIZE = 16
_CONFIG_PATH = DATA_DIR / "config.json"
if not _CONFIG_PATH.exists():
_CONFIG_PATH = PROJECT_ROOT / "config.json"
logger.warning(f"No config.json in {DATA_DIR} — using root config.json as fallback.")
with open(_CONFIG_PATH, encoding="utf-8") as _f:
_config = json.load(_f)
EMBEDDING_ENDPOINT_URL = _config["embedding"]["endpoint_url"]
EMBEDDING_MODEL_NAME = _config["embedding"]["model"]
AZURE_API_KEY = os.environ.get("AZURE_API_KEY")
if not AZURE_API_KEY:
logger.error("AZURE_API_KEY is not set. Export it before running this script.")
sys.exit(1)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def extract_text_from_pdf(pdf_path: Path) -> str:
reader = PdfReader(str(pdf_path))
pages_text = []
for page_num, page in enumerate(reader.pages, start=1):
text = page.extract_text()
if text and text.strip():
pages_text.append(f"[Page {page_num}]\n{text.strip()}")
return "\n\n".join(pages_text)
def chunk_text(text: str, source: str) -> list[dict]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(text)
return [{"text": chunk, "source": source, "chunk_index": i} for i, chunk in enumerate(chunks)]
def generate_embeddings(texts: list[str]) -> list[list[float]]:
headers = {"api-key": AZURE_API_KEY, "Content-Type": "application/json"}
payload = {"input": texts, "model": EMBEDDING_MODEL_NAME}
resp = http_requests.post(EMBEDDING_ENDPOINT_URL, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
data = resp.json()
return [item["embedding"] for item in data["data"]]
def generate_embeddings_batched(texts: list[str]) -> list[list[float]]:
all_embeddings = []
for i in range(0, len(texts), EMBEDDING_BATCH_SIZE):
batch = texts[i:i + EMBEDDING_BATCH_SIZE]
logger.info(f" Embedding batch {i // EMBEDDING_BATCH_SIZE + 1} ({len(batch)} chunks)...")
embeddings = generate_embeddings(batch)
all_embeddings.extend(embeddings)
time.sleep(0.5) # Rate-limit courtesy
return all_embeddings
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if not TRAIN_DATA_DIR.exists():
logger.error(f"Training data directory not found: {TRAIN_DATA_DIR}")
sys.exit(1)
logger.info(f"Training data directory: {TRAIN_DATA_DIR}")
logger.info(f"ChromaDB path: {CHROMA_PERSIST_DIR}")
logger.info(f"Embedding model: {EMBEDDING_MODEL_NAME}")
chroma_client = chromadb.PersistentClient(
path=CHROMA_PERSIST_DIR,
settings=Settings(anonymized_telemetry=False),
)
collection = chroma_client.get_or_create_collection(
name=COLLECTION_NAME,
metadata={"hnsw:space": "cosine"},
)
logger.info(f"ChromaDB collection '{COLLECTION_NAME}' — existing documents: {collection.count()}")
# Gather all PDF and TXT files recursively
files = sorted(
list(TRAIN_DATA_DIR.rglob("*.pdf")) + list(TRAIN_DATA_DIR.rglob("*.txt"))
)
logger.info(f"Found {len(files)} files to process.")
total_chunks_added = 0
for idx, file_path in enumerate(files, start=1):
relative = file_path.relative_to(TRAIN_DATA_DIR)
# Use category/filename as the source identifier
source = str(relative)
logger.info(f"[{idx}/{len(files)}] Processing: {source}")
try:
if file_path.suffix.lower() == ".pdf":
text = extract_text_from_pdf(file_path)
else:
text = file_path.read_text(encoding="utf-8")
except Exception as e:
logger.warning(f" Skipped (read error): {e}")
continue
if not text.strip():
logger.warning(f" Skipped (no extractable text)")
continue
chunks = chunk_text(text, source=source)
if not chunks:
logger.warning(f" Skipped (no chunks produced)")
continue
logger.info(f" {len(chunks)} chunks extracted")
texts = [c["text"] for c in chunks]
try:
embeddings = generate_embeddings_batched(texts)
except http_requests.exceptions.HTTPError as e:
logger.error(f" Embedding failed: {e}")
continue
except Exception as e:
logger.error(f" Unexpected embedding error: {e}")
continue
existing_count = collection.count()
ids = [f"doc_{existing_count + i}" for i in range(len(chunks))]
metadatas = [{"source": c["source"], "chunk_index": c["chunk_index"]} for c in chunks]
collection.add(
ids=ids,
embeddings=embeddings,
documents=texts,
metadatas=metadatas,
)
total_chunks_added += len(chunks)
logger.info(f" Added {len(chunks)} chunks (total in store: {collection.count()})")
logger.info("=" * 60)
logger.info(f"Ingestion complete. Chunks added this run: {total_chunks_added}")
logger.info(f"Total documents in vector store: {collection.count()}")
if __name__ == "__main__":
main()