from functools import lru_cache from pathlib import Path from typing import Literal from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict # Single source of truth for the deployed version. app.py, admin.py and all # outbound User-Agent strings read this constant - never hardcode versions elsewhere. LIBBEE_VERSION = "3.8.8" class Settings(BaseSettings): model_config = SettingsConfigDict( case_sensitive=False, env_file=".env", env_file_encoding="utf-8", extra="ignore", ) openai_api_key: str = "" anthropic_api_key: str = "" primo_api_key: str = "" admin_password: str = "" session_secret: str = "change-me" environment: Literal["development", "production"] = "production" maintenance_mode: bool = False max_results: int = 5 runtime_dir: str = "/tmp/libbee_runtime" feedback_store_path: str = "" metrics_store_path: str = "" config_store_path: str = "" rag_index_dir: str = "" knowledge_dir: str = "" rate_limit_per_minute: int = 40 # Cloudflare Worker URL for persistent analytics # Set in HF Space secrets (never hardcode the URL in this repository). cloudflare_worker_url: str = "" # Shared-secret bearer token for the Worker analytics endpoints (/log, /analytics*, /reports*). # Must match the ANALYTICS_TOKEN secret configured on the Cloudflare Worker. cloudflare_worker_token: str = "" # Contact for polite-pool identification with open scholarly APIs (OpenAlex, Unpaywall, Crossref). contact_email: str = "libse@ku.ac.ae" @field_validator("max_results") @classmethod def reasonable_max_results(cls, v: int) -> int: if not 1 <= v <= 20: raise ValueError("max_results must be between 1 and 20") return v @field_validator("rate_limit_per_minute") @classmethod def reasonable_rate_limit(cls, v: int) -> int: if not 5 <= v <= 600: raise ValueError("rate_limit_per_minute must be between 5 and 600") return v @property def runtime_path(self) -> Path: return Path(self.runtime_dir) @property def feedback_path(self) -> Path: return Path(self.feedback_store_path) if self.feedback_store_path else self.runtime_path / "feedback.jsonl" @property def metrics_path(self) -> Path: return Path(self.metrics_store_path) if self.metrics_store_path else self.runtime_path / "metrics.json" @property def config_path(self) -> Path: return Path(self.config_store_path) if self.config_store_path else self.runtime_path / "runtime_config.json" @property def rag_cache_dir(self) -> Path: return Path(self.rag_index_dir) if self.rag_index_dir else self.runtime_path / "rag_cache" @property def kb_dir(self) -> Path: if self.knowledge_dir: return Path(self.knowledge_dir) if Path("/app/data/knowledge").exists(): return Path("/app/data/knowledge") return Path("data/knowledge") @lru_cache def get_settings() -> Settings: settings = Settings() settings.runtime_path.mkdir(parents=True, exist_ok=True) settings.rag_cache_dir.mkdir(parents=True, exist_ok=True) return settings