| """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 | |