""" Application configuration using Pydantic Settings. Following best practices: modular settings, environment-based config. """ from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict # Read version from pyproject.toml try: import tomllib _pyproject_path = Path(__file__).parent.parent / "pyproject.toml" with open(_pyproject_path, "rb") as f: _pyproject = tomllib.load(f) __version__ = _pyproject["project"]["version"] except Exception: __version__ = "0.0.0" # Fallback if pyproject.toml not found class Settings(BaseSettings): """Application settings loaded from environment variables.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # API Configuration library_api_key: str = Field(..., description="API key for authentication") # Neo4j Configuration neo4j_uri: str = Field(default="bolt://neo4j:7687", description="Neo4j Bolt URI") neo4j_user: str = Field(default="neo4j", description="Neo4j username") neo4j_password: str = Field(..., description="Neo4j password") # Qdrant Configuration qdrant_host: str = Field(default="qdrant", description="Qdrant host") qdrant_port: int = Field(default=6333, description="Qdrant port") # Wiki.js Configuration wikijs_url: str = Field(default="http://wiki:3000", description="Wiki.js URL") wikijs_username: str = Field(..., description="Wiki.js username") wikijs_password: str = Field(..., description="Wiki.js password") # Wiki.js Database Configuration (for change listener) wikijs_db_host: str = Field(default="postgres-shared", description="Wiki.js PostgreSQL host") wikijs_db_port: int = Field(default=5432, description="Wiki.js PostgreSQL port") wikijs_db_name: str = Field(default="library", description="Wiki.js database name") wikijs_db_user: str = Field(default="library_desk_listener", description="Wiki.js database user (read-only)") wikijs_db_password: str = Field(..., description="Wiki.js database password") wikijs_change_listener_debounce_seconds: int = Field( default=5, ge=1, le=60, description="Debounce duration to prevent processing duplicate notifications" ) # SearXNG Configuration searxng_url: str = Field(default="http://searxng:8080", description="SearXNG URL") # Ollama Configuration (for embeddings) ollama_url: str = Field(default="http://ollama:11434", description="Ollama URL") ollama_model: str = Field(default="nomic-embed-text", description="Ollama embedding model") # HybridRAG Configuration reranker_model: str = Field(default="mistral-nemo", description="Model for LLM re-ranking") reranker_enabled: bool = Field(default=True, description="Enable LLM re-ranking") hybrid_rag_vector_limit: int = Field(default=10, ge=1, le=50, description="Vector search limit") hybrid_rag_graph_limit: int = Field(default=10, ge=1, le=50, description="Graph search limit") hybrid_rag_web_limit: int = Field(default=5, ge=1, le=20, description="Web search limit") vector_similarity_threshold: float = Field(default=0.7, ge=0.0, le=1.0, description="Minimum similarity score for vector results") # Entity Linking Fuzzy Matching Configuration entity_linking_min_confidence: float = Field(default=0.70, ge=0.0, le=1.0, description="Minimum confidence for entity-document matching") entity_linking_min_entity_length: int = Field(default=5, ge=1, le=50, description="Minimum entity name length for matching") entity_linking_min_containment_ratio: float = Field(default=0.30, ge=0.0, le=1.0, description="Minimum containment ratio for substring matching") entity_linking_min_token_overlap: float = Field(default=0.60, ge=0.0, le=1.0, description="Minimum token overlap ratio for matching") # Redis Configuration (for job tracking - separate DB from wiki) redis_host: str = Field(default="redis-shared", description="Redis host") redis_port: int = Field(default=6379, description="Redis port") redis_db: int = Field(default=4, description="Redis database number (4 for library-desk jobs)") # Application app_name: str = Field(default="Library Desk", description="Application name") app_version: str = Field(default=__version__, description="Application version") debug: bool = Field(default=False, description="Debug mode") # RAG Search Configuration search_cache_ttl: int = Field(default=300, ge=0, le=3600, description="Search cache TTL in seconds") search_timeout: int = Field(default=10, ge=1, le=60, description="SearXNG timeout in seconds") search_default_limit: int = Field(default=10, ge=1, le=20, description="Default number of search results") # Content Extraction Configuration content_extraction_timeout: int = Field(default=5, ge=1, le=30, description="Trafilatura per-URL timeout in seconds") content_max_length: int = Field(default=2000, ge=500, le=10000, description="Max extracted content length per result") @property def qdrant_url(self) -> str: """Computed Qdrant URL.""" return f"http://{self.qdrant_host}:{self.qdrant_port}" @property def redis_url(self) -> str: """Computed Redis URL.""" return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}" @lru_cache def get_settings() -> Settings: """ Get cached settings instance. Uses lru_cache to ensure single instance across app. """ return Settings()