skin-lesion-api / src /rag.py
Ranjith445's picture
Initial HF Space deploy
e4ee3e8
Raw
History Blame Contribute Delete
4.71 kB
import os
import sys
import pickle
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import (
KNOWLEDGE_DIR, VECTORSTORE_DIR,
EMBEDDING_MODEL, CHUNK_SIZE, CHUNK_OVERLAP, TOP_K_RETRIEVAL,
)
os.makedirs(VECTORSTORE_DIR, exist_ok=True)
INDEX_PATH = os.path.join(VECTORSTORE_DIR, "faiss.index")
CHUNKS_PATH = os.path.join(VECTORSTORE_DIR, "chunks.pkl")
_embedder = None
_index = None
_chunks = None
def _get_embedder():
global _embedder
if _embedder is None:
_embedder = SentenceTransformer(EMBEDDING_MODEL)
return _embedder
# ── Chunking ───────────────────────────────────────────────────────────────────
def _chunk_text(text: str, source: str) -> list[dict]:
words = text.split()
chunks = []
step = CHUNK_SIZE - CHUNK_OVERLAP
for i in range(0, len(words), step):
chunk_words = words[i: i + CHUNK_SIZE]
if len(chunk_words) < 20:
continue
chunks.append({"text": " ".join(chunk_words), "source": source})
return chunks
# ── Build vectorstore ──────────────────────────────────────────────────────────
def build_vectorstore(force: bool = False):
if not force and os.path.exists(INDEX_PATH) and os.path.exists(CHUNKS_PATH):
print("[RAG] Vectorstore already exists. Skipping build.")
return
embedder = _get_embedder()
all_chunks = []
print("[RAG] Loading knowledge docs...")
for fname in sorted(os.listdir(KNOWLEDGE_DIR)):
if not fname.endswith(".txt"):
continue
fpath = os.path.join(KNOWLEDGE_DIR, fname)
with open(fpath, "r", encoding="utf-8") as f:
text = f.read()
chunks = _chunk_text(text, source=fname.replace(".txt", ""))
all_chunks.extend(chunks)
print(f" {fname}: {len(chunks)} chunks")
print(f"\n[RAG] Total chunks: {len(all_chunks)}")
print("[RAG] Encoding chunks...")
texts = [c["text"] for c in all_chunks]
embeddings = embedder.encode(texts, show_progress_bar=True, batch_size=32)
embeddings = embeddings.astype(np.float32)
# Normalize for cosine similarity
faiss.normalize_L2(embeddings)
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim) # Inner product = cosine after normalization
index.add(embeddings)
faiss.write_index(index, INDEX_PATH)
with open(CHUNKS_PATH, "wb") as f:
pickle.dump(all_chunks, f)
print(f"[RAG] Vectorstore saved: {index.ntotal} vectors dim={dim}")
# ── Load vectorstore ───────────────────────────────────────────────────────────
def _load_vectorstore():
global _index, _chunks
if _index is None:
if not os.path.exists(INDEX_PATH):
raise FileNotFoundError("Vectorstore not found. Run build_vectorstore() first.")
_index = faiss.read_index(INDEX_PATH)
with open(CHUNKS_PATH, "rb") as f:
_chunks = pickle.load(f)
# ── Retrieve ───────────────────────────────────────────────────────────────────
def retrieve(query: str, top_k: int = TOP_K_RETRIEVAL) -> list[dict]:
_load_vectorstore()
embedder = _get_embedder()
q_embed = embedder.encode([query]).astype(np.float32)
faiss.normalize_L2(q_embed)
scores, idxs = _index.search(q_embed, top_k)
results = []
for score, idx in zip(scores[0], idxs[0]):
if idx == -1:
continue
chunk = _chunks[idx].copy()
chunk["score"] = round(float(score), 4)
results.append(chunk)
return results
# ── CLI test ───────────────────────────────────────────────────────────────────
if __name__ == "__main__":
build_vectorstore(force=True)
print("\n[RAG] Test retrieval: 'melanoma ABCDE dermoscopy'")
results = retrieve("melanoma ABCDE dermoscopy signs")
for i, r in enumerate(results, 1):
print(f"\n [{i}] source={r['source']} score={r['score']}")
print(f" {r['text'][:200]}...")