File size: 6,568 Bytes
454f118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923d3a8
454f118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
config/settings.py
==================
Central configuration for the entire RAG pipeline.
All tunable hyperparameters live here — no magic numbers scattered in code.
Uses pydantic-settings to load from .env file automatically.

IMPORTANT: This file sets USE_TF=0 / USE_TORCH=1 at import time to prevent
the broken TensorFlow installation on this machine from crashing sentence_transformers.
Always import config.settings BEFORE importing sentence_transformers or transformers.
"""

import os
# Must be set BEFORE importing transformers / sentence_transformers
os.environ.setdefault("USE_TF", "0")
os.environ.setdefault("USE_TORCH", "1")

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
from pathlib import Path


class Settings(BaseSettings):
    """
    Production RAG Configuration.
    Values are loaded from .env file (override defaults by editing .env).
    """

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
    )

    # ------------------------------------------------------------------ #
    # Paths
    # ------------------------------------------------------------------ #
    raw_pdfs_path: Path = Field(default=Path("./raw_pdfs"))
    qdrant_path: Path = Field(default=Path("./data/qdrant_store"))
    bm25_index_path: Path = Field(default=Path("./data/bm25_index"))
    processed_docs_path: Path = Field(default=Path("./data/processed"))

    # ------------------------------------------------------------------ #
    # Models
    # ------------------------------------------------------------------ #
    groq_api_key: str = Field(default="")
    groq_model: str = Field(default="openai/gpt-oss-120b")

    embedding_model: str = Field(default="BAAI/bge-large-en-v1.5")
    embedding_dim: int = Field(default=1024)          # bge-large output dimension (NOT 768 — that's bge-base)

    reranker_model: str = Field(default="BAAI/bge-reranker-base")

    device: str = Field(default="auto")              # auto | cuda | cpu

    # ------------------------------------------------------------------ #
    # Qdrant / Vector Store
    # ------------------------------------------------------------------ #
    qdrant_url: str = Field(default="")
    qdrant_api_key: str = Field(default="")
    qdrant_collection: str = Field(default="financial_rag")

    # HNSW index params
    hnsw_m: int = Field(default=16)                  # connections per node
    hnsw_ef_construct: int = Field(default=200)      # build accuracy

    # ------------------------------------------------------------------ #
    # Chunking Hyperparameters
    # ------------------------------------------------------------------ #
    # Child chunks (used for retrieval / indexing)
    child_chunk_size: int = Field(default=350)        # tokens
    child_chunk_overlap: int = Field(default=50)      # tokens (~14% overlap)

    # Parent chunks (returned to LLM as context)
    parent_chunk_size: int = Field(default=1800)      # tokens
    parent_chunk_overlap: int = Field(default=100)    # tokens

    # Minimum chunk size — discard orphan fragments
    min_chunk_tokens: int = Field(default=80)

    # Font size threshold to detect headings in PyMuPDF
    heading_font_size_threshold: float = Field(default=12.0)

    # ------------------------------------------------------------------ #
    # Retrieval Hyperparameters
    # ------------------------------------------------------------------ #
    # How many candidates to retrieve at each stage
    dense_top_k: int = Field(default=30)             # dense retrieval candidates
    sparse_top_k: int = Field(default=30)            # BM25 candidates
    rrf_k: int = Field(default=60)                   # RRF constant (standard = 60)
    rerank_top_n: int = Field(default=5)             # final chunks after reranking

    # Query expansion: number of paraphrased queries to generate
    num_expanded_queries: int = Field(default=3)

    # Score threshold — discard chunks below this similarity
    score_threshold: float = Field(default=0.35)

    # ------------------------------------------------------------------ #
    # LLM Generation
    # ------------------------------------------------------------------ #
    llm_temperature: float = Field(default=0.1)      # low = more factual
    llm_max_tokens: int = Field(default=2048)
    llm_context_window: int = Field(default=8192)

    # ------------------------------------------------------------------ #
    # Embedding Batch Processing
    # ------------------------------------------------------------------ #
    embed_batch_size: int = Field(default=32)

    # ------------------------------------------------------------------ #
    # Metadata / Fiscal Year
    # ------------------------------------------------------------------ #
    # BSE India: Financial year = April to March
    # Reports filed Apr–Jun 2025 → FY2024-25
    default_fiscal_year: str = Field(default="FY2025")

    # Known company name → ticker mapping (for metadata tagging)
    company_ticker_map: dict = Field(default={
        "AIRTEL": "BHARTIARTL",
        "AXIS": "AXISBANK",
        "BAJAJ": "BAJFINANCE",
        "HCL": "HCLTECH",
        "HDFC": "HDFCBANK",
        "HU": "HINDUNILVR",
        "ICICI": "ICICIBANK",
        "INFOSYS": "INFY",
        "ITC": "ITC",
        "KOTAK": "KOTAKBANK",
        "KVB": "KARURVYSYA",
        "L&T": "LT",
        "MARUTI": "MARUTI",
        "MRF": "MRF",
        "ONGC": "ONGC",
        "REL": "RELIANCE",
        "SBI": "SBIN",
        "TCS": "TCS",
    })

    # ------------------------------------------------------------------ #
    # Logging
    # ------------------------------------------------------------------ #
    log_level: str = Field(default="INFO")

    def get_device(self) -> str:
        """Resolve 'auto' to actual device string."""
        if self.device == "auto":
            try:
                import torch
                return "cuda" if torch.cuda.is_available() else "cpu"
            except ImportError:
                return "cpu"
        return self.device

    def ensure_dirs(self) -> None:
        """Create all required data directories if they don't exist."""
        dirs = [
            self.qdrant_path,
            self.bm25_index_path,
            self.processed_docs_path,
        ]
        for d in dirs:
            d.mkdir(parents=True, exist_ok=True)


# Singleton settings instance — import this everywhere
settings = Settings()