from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import Field, field_validator from typing import Optional import os from .definitions import MAX_FILE_SIZE, MAX_TOTAL_SIZE, ALLOWED_TYPES def _default_chroma_path() -> str: if os.environ.get("SPACE_ID"): # Hugging Face Spaces return os.environ.get("CHROMA_DB_PATH", "/tmp/chroma_db") return os.environ.get("CHROMA_DB_PATH", "./chroma_db") def _is_hf() -> bool: return os.environ.get("SPACE_ID") is not None class Settings(BaseSettings): """ Application parameters loaded from environment variables. For local development: Create a .env file in the project root with your configuration: GOOGLE_API_KEY=your_api_key_here For Hugging Face Spaces: Add GOOGLE_API_KEY as a secret in Space Settings > Repository secrets """ model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, ) # File upload parameters with defaults from definitions MAX_FILE_SIZE: int = MAX_FILE_SIZE MAX_TOTAL_SIZE: int = MAX_TOTAL_SIZE ALLOWED_TYPES: list = ALLOWED_TYPES # API keys - REQUIRED, must be set via environment variable or HF Secrets GOOGLE_API_KEY: str = Field( ..., # Required field description="Google API key for Gemini models", ) # Database parameters CHROMA_DB_PATH: str = Field(default_factory=_default_chroma_path) # Chunking parameters CHUNK_SIZE: int = 2000 CHUNK_OVERLAP: int = 100 # Retriever parameters VECTOR_SEARCH_K: int = 25 VECTOR_SEARCH_K_CHROMA: int = 15 # Fixed typo: was VECTOR_Search_K_CHROMA VECTOR_FETCH_K: int = 35 VECTOR_SCORE_THRESHOLD: float = 0.3 BM25_SEARCH_K: int = 8 HYBRID_RETRIEVER_WEIGHTS: list = [0.4, 0.6] # [BM25 weight, Vector weight] CHROMA_COLLECTION_NAME: str = "documents" # Workflow parameters MAX_RESEARCH_ATTEMPTS: int = 5 ENABLE_QUERY_REWRITING: bool = True MAX_QUERY_REWRITES: int = 1 RELEVANCE_CHECK_K: int = 20 # Research agent parameters RESEARCH_TOP_K: int = 15 RESEARCH_MAX_CONTEXT_CHARS: int = Field(default_factory=lambda: 800_000 if _is_hf() else 8000000000) RESEARCH_MAX_OUTPUT_TOKENS: int = 500 NUM_RESEARCH_CANDIDATES: int = 2 # Number of research questions to generate # Verification parameters VERIFICATION_MAX_CONTEXT_CHARS: int = Field(default_factory=lambda: 300_000 if _is_hf() else 800000000) VERIFICATION_MAX_OUTPUT_TOKENS: int = 300 # Logging parameters LOG_LEVEL: str = "INFO" # Cache parameters CACHE_DIR: str = "document_cache" CACHE_EXPIRE_DAYS: int = 7 # LLM parameters LLM_MAX_RETRIES: int = 3 LLM_RETRY_DELAY: float = 1.0 LLM_MODEL_NAME: str = "gemini-2.5-flash-lite" # Default model for all agents # Agent-specific LLM models (override LLM_MODEL_NAME if needed) RESEARCH_AGENT_MODEL: str = "gemini-2.5-flash-lite" VERIFICATION_AGENT_MODEL: str = "gemini-2.5-flash-lite" RELEVANCE_CHECKER_MODEL: str = "gemini-2.5-flash-lite" # Chart extraction parameters ENABLE_CHART_EXTRACTION: bool = True CHART_VISION_MODEL: str = "gemini-2.5-flash-lite" CHART_MAX_TOKENS: int = 1500 CHART_DPI: int = Field(default_factory=lambda: 110 if _is_hf() else 110) # Lower DPI saves memory CHART_BATCH_SIZE: int = Field(default_factory=lambda: 1 if _is_hf() else 1) # Process pages in batches CHART_MAX_IMAGE_SIZE: int = Field(default_factory=lambda: 1200 if _is_hf() else 1200) # Max dimension for images # Local chart detection parameters (cost optimization) CHART_USE_LOCAL_DETECTION: bool = Field(default_factory=lambda: True if _is_hf() else True) # Use OpenCV first (FREE) CHART_MIN_CONFIDENCE: float = 0.4 # Only analyze charts with confidence > 40% CHART_SKIP_GEMINI_DETECTION: bool = True # Skip Gemini for detection, only use for analysis CHART_GEMINI_FALLBACK_ENABLED: bool = False # Optional: Use Gemini if local fails # Gemini batch processing parameters (speed optimization - 2-3× faster) CHART_GEMINI_BATCH_SIZE: int = 1 # Analyze 1 chart per API call (reduced from 2 for reliability) CHART_ENABLE_BATCH_ANALYSIS: bool = True # Enable batch processing for speed @field_validator("GOOGLE_API_KEY") @classmethod def validate_api_key(cls, v: str) -> str: """Validate that API key is provided and not a placeholder.""" if not v or v.strip() == "": raise ValueError("GOOGLE_API_KEY is required. Set it in your .env file or HF Secrets.") if v.startswith("your_") or v == "YOUR_API_KEY_HERE": raise ValueError("Please replace the placeholder GOOGLE_API_KEY with your actual API key.") return v def _get_parameters(): """Get parameters instance with helpful error messages.""" is_hf_space = os.environ.get("SPACE_ID") is not None try: return Settings() except Exception as e: import sys print(f"⚠️ Configuration Error: {e}", file=sys.stderr) if is_hf_space: print("💡 Tip: Add GOOGLE_API_KEY in Space Settings > Repository secrets", file=sys.stderr) else: print("💡 Tip: Create a .env file with GOOGLE_API_KEY=your_api_key", file=sys.stderr) raise # Create parameters instance parameters = _get_parameters()