twin / rag.py
KUSHT07's picture
Upload 16 files
3618b44 verified
Raw
History Blame Contribute Delete
4.1 kB
"""ChromaDB-backed RAG: chunk knowledge files, embed, retrieve."""
from __future__ import annotations
import uuid
from pathlib import Path
import chromadb
import config
from chunking import chunk_text
ROOT = Path(__file__).parent
KNOWLEDGE_DIR = ROOT / "knowledge"
COLLECTION_NAME = "kush_memo"
DOCUMENTS = [
{"file": "identity.md", "source": "Identity and Personal Context"},
{"file": "career.md", "source": "Career History"},
{"file": "technical.md", "source": "Technical Stack"},
]
_chroma_client: chromadb.ClientAPI | None = None
_collection: chromadb.Collection | None = None
def _get_chroma_client() -> chromadb.ClientAPI:
global _chroma_client
if _chroma_client is None:
config.CHROMA_PATH.mkdir(parents=True, exist_ok=True)
_chroma_client = chromadb.PersistentClient(path=str(config.CHROMA_PATH))
return _chroma_client
def get_collection() -> chromadb.Collection:
global _collection
if _collection is None:
_collection = _get_chroma_client().get_or_create_collection(COLLECTION_NAME)
return _collection
def _load_documents() -> list[dict[str, str]]:
docs: list[dict[str, str]] = []
for spec in DOCUMENTS:
path = KNOWLEDGE_DIR / spec["file"]
if not path.is_file():
raise FileNotFoundError(f"Knowledge file not found: {path}")
docs.append({"text": path.read_text(encoding="utf-8"), "source": spec["source"]})
return docs
def _chunk_documents(documents: list[dict[str, str]]) -> tuple[list[str], list[str], list[dict]]:
chunks: list[str] = []
ids: list[str] = []
metadatas: list[dict] = []
for doc in documents:
doc_chunks = chunk_text(
doc["text"],
chunk_size=config.RAG_CHUNK_SIZE,
overlap=config.RAG_CHUNK_OVERLAP,
)
ids.extend(str(uuid.uuid4()) for _ in doc_chunks)
metadatas.extend(
{"source": doc["source"], "chunk_index": i} for i in range(len(doc_chunks))
)
chunks.extend(doc_chunks)
return chunks, ids, metadatas
def embed_texts(texts: list[str]) -> list[list[float]]:
client = config.ensure_openai_client()
response = client.embeddings.create(
input=texts,
model=config.EMBEDDING_MODEL,
)
return [item.embedding for item in response.data]
def build_index(*, force_rebuild: bool = False) -> int:
"""Index knowledge files into ChromaDB. Returns number of chunks stored."""
config.ensure_openai_client()
collection = get_collection()
if force_rebuild:
existing = collection.get()["ids"]
if existing:
collection.delete(ids=existing)
if collection.count() > 0 and not force_rebuild:
return collection.count()
documents = _load_documents()
chunks, ids, metadatas = _chunk_documents(documents)
if not chunks:
return 0
embeddings = embed_texts(chunks)
collection.add(
ids=ids,
documents=chunks,
embeddings=embeddings,
metadatas=metadatas,
)
return len(chunks)
def ensure_index() -> None:
"""Build the vector index when the collection is empty."""
if get_collection().count() == 0:
count = build_index()
print(f"Built RAG index with {count} chunks.")
def retrieve(query: str, n_results: int | None = None) -> tuple[str, list[dict]]:
"""Return joined context text and retrieval metadata for a user query."""
config.ensure_openai_client()
collection = get_collection()
if collection.count() == 0:
return "", []
n = n_results if n_results is not None else config.RAG_N_RESULTS
query_embedding = embed_texts([query])[0]
results = collection.query(
query_embeddings=[query_embedding],
n_results=min(n, collection.count()),
)
documents = results["documents"][0]
metadatas = results["metadatas"][0]
context = "\n\n".join(documents)
return context, metadatas