Source code for rag.core.config

"""Configuration for the papers RAG pipeline."""

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import Final

from dotenv import load_dotenv

from rag.core.strategy import RAG_STRATEGY_ENV, RagStrategy, index_dir_for

[docs] PAPERS_DIR_ENV: Final = "PAPERS_DIR"
[docs] CHROMA_DIR_ENV: Final = "CHROMA_DIR"
[docs] CHROMA_COLLECTION_ENV: Final = "CHROMA_COLLECTION"
[docs] EMBED_MODEL_ENV: Final = "EMBED_MODEL"
[docs] OPENAI_API_KEY_ENV: Final = "OPENAI_API_KEY"
[docs] OPENAI_MODEL_ENV: Final = "OPENAI_MODEL"
[docs] CHUNK_SIZE_ENV: Final = "CHUNK_SIZE"
[docs] CHUNK_OVERLAP_ENV: Final = "CHUNK_OVERLAP"
[docs] SIMILARITY_TOP_K_ENV: Final = "SIMILARITY_TOP_K"
[docs] DEFAULT_COLLECTION_NAME: Final = "papers"
[docs] DEFAULT_EMBED_MODEL: Final = "BAAI/bge-small-en-v1.5"
[docs] DEFAULT_LLM_MODEL: Final = "gpt-4o-mini"
[docs] DEFAULT_CHUNK_SIZE: Final = 1024
[docs] DEFAULT_CHUNK_OVERLAP: Final = 128
[docs] DEFAULT_SIMILARITY_TOP_K: Final = 5
[docs] def find_repo_root(*, start: Path | None = None) -> Path: """Walk parents until the monorepo root (contains ``libs/rag-core``) is found. Parameters ---------- start : Path or None Directory to start from. Defaults to this file's location. Returns ------- Path Absolute path to the monorepo root. Raises ------ FileNotFoundError If no ancestor contains ``libs/rag-core``. """ current = (start or Path(__file__).resolve()).resolve() if current.is_file(): current = current.parent for candidate in (current, *current.parents): if (candidate / "libs" / "rag-core").is_dir(): return candidate raise FileNotFoundError( "Could not locate monorepo root (expected libs/rag-core). " "Pass RagConfig paths explicitly or run from inside the repo.", )
[docs] def load_repo_dotenv() -> None: """Load ``.env`` from cwd and the monorepo root. Existing process environment wins (``override=False``). Streamlit sets cwd to the script directory, so a repo-root ``.env`` is missed by a bare ``load_dotenv()``. """ load_dotenv() try: load_dotenv(find_repo_root() / ".env") except FileNotFoundError: return
@dataclass(frozen=True, kw_only=True)
[docs] class RagConfig: """Runtime settings for ingest + query. Attributes ---------- papers_dir : Path Directory of PDF papers to ingest. chroma_dir : Path Persistent Chroma directory (created if missing). collection_name : str Chroma collection name. embed_model_name : str HuggingFace embedding model id (local). llm_model_name : str OpenAI chat model id. chunk_size : int SentenceSplitter chunk size. chunk_overlap : int SentenceSplitter overlap. similarity_top_k : int Retrieval top-k for the query engine. strategy : RagStrategy In-process orchestration backend (LlamaIndex or LangChain). """
[docs] papers_dir: Path
[docs] chroma_dir: Path
[docs] collection_name: str = DEFAULT_COLLECTION_NAME
[docs] embed_model_name: str = DEFAULT_EMBED_MODEL
[docs] llm_model_name: str = DEFAULT_LLM_MODEL
[docs] chunk_size: int = DEFAULT_CHUNK_SIZE
[docs] chunk_overlap: int = DEFAULT_CHUNK_OVERLAP
[docs] similarity_top_k: int = DEFAULT_SIMILARITY_TOP_K
[docs] strategy: RagStrategy = RagStrategy.LLAMAINDEX
@classmethod
[docs] def from_env( cls, *, repo_root: Path | None = None, strategy: RagStrategy | None = None, ) -> RagConfig: """Build config from environment variables with repo-relative defaults. Parameters ---------- repo_root : Path or None Monorepo root. Discovered automatically when omitted. strategy : RagStrategy or None Orchestration backend. Defaults to ``RAG_STRATEGY`` or LlamaIndex. Returns ------- RagConfig Resolved configuration. """ root = repo_root or find_repo_root() chosen = strategy or RagStrategy( os.environ.get(RAG_STRATEGY_ENV, RagStrategy.LLAMAINDEX.value), ) papers = Path( os.environ.get(PAPERS_DIR_ENV, root / "assets" / "pdf" / "papers"), ) chroma_raw = os.environ.get(CHROMA_DIR_ENV) chroma = Path(chroma_raw) if chroma_raw else index_dir_for(repo_root=root, strategy=chosen) if not papers.is_absolute(): papers = (root / papers).resolve() if not chroma.is_absolute(): chroma = (root / chroma).resolve() return cls( papers_dir=papers, chroma_dir=chroma, collection_name=os.environ.get( CHROMA_COLLECTION_ENV, DEFAULT_COLLECTION_NAME, ), embed_model_name=os.environ.get(EMBED_MODEL_ENV, DEFAULT_EMBED_MODEL), llm_model_name=os.environ.get(OPENAI_MODEL_ENV, DEFAULT_LLM_MODEL), chunk_size=int( os.environ.get(CHUNK_SIZE_ENV, str(DEFAULT_CHUNK_SIZE)), ), chunk_overlap=int( os.environ.get(CHUNK_OVERLAP_ENV, str(DEFAULT_CHUNK_OVERLAP)), ), similarity_top_k=int( os.environ.get(SIMILARITY_TOP_K_ENV, str(DEFAULT_SIMILARITY_TOP_K)), ), strategy=chosen, )
[docs] def ensure_dirs(self) -> None: """Create the Chroma persistence directory if needed.""" self.chroma_dir.mkdir(parents=True, exist_ok=True)