from __future__ import annotations import os from pathlib import Path from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent load_dotenv(BASE_DIR / ".env") def _env_path(name: str, default: Path) -> Path: value = os.getenv(name, "").strip() return Path(value).expanduser() if value else default def _env_users(name: str, default: str = "") -> set[str]: raw = os.getenv(name, default) return { value.strip().casefold() for value in raw.split(",") if value.strip() } # En local, Atlas conserve son organisation historique. # Dans le Docker Space, Dockerfile définit ATLAS_DATA_ROOT=/data : # le Bucket monté devient le stockage permanent de l'application. # # Important : LanceDB ne doit pas écrire directement dans ce Bucket monté # via FUSE, car ses commits utilisent des renommages atomiques non pris en # charge. La base LanceDB active travaille donc sur le disque local du # conteneur, tandis que les fragments vectorisés sont sauvegardés en Parquet # dans le Bucket et rechargés automatiquement au démarrage. _data_root_raw = os.getenv("ATLAS_DATA_ROOT", "").strip() if _data_root_raw: DATA_DIR = Path(_data_root_raw).expanduser() RUNTIME_DIR = _env_path( "ATLAS_RUNTIME_ROOT", Path("/tmp/atlas-rag"), ) DOCUMENTS_DIR = _env_path("DOCUMENTS_DIR", DATA_DIR / "documents") UPLOADS_DIR = _env_path("UPLOADS_DIR", DOCUMENTS_DIR) else: DATA_DIR = _env_path("DATA_DIR", BASE_DIR / "data") RUNTIME_DIR = _env_path("ATLAS_RUNTIME_ROOT", DATA_DIR) DOCUMENTS_DIR = _env_path("DOCUMENTS_DIR", BASE_DIR / "documents") UPLOADS_DIR = _env_path("UPLOADS_DIR", DATA_DIR / "uploads") # Base active, modifiable : disque local POSIX du conteneur. LANCEDB_DIR = _env_path("LANCEDB_DIR", RUNTIME_DIR / "lancedb") INDEX_STATE_PATH = _env_path( "INDEX_STATE_PATH", RUNTIME_DIR / "index_state.json", ) # SQLite doit rester sur le disque local du conteneur. Le Storage Bucket # Hugging Face est un montage distant et ne fournit pas toujours les # primitives de verrouillage/journalisation nécessaires à SQLite (WAL). # Le catalogue est reconstruit au démarrage depuis les PDF et les fragments # persistants du Bucket. if _data_root_raw: STATE_DB_PATH = RUNTIME_DIR / "state.sqlite3" else: STATE_DB_PATH = _env_path( "STATE_DB_PATH", DATA_DIR / "state.sqlite3", ) # Données persistantes : Storage Bucket Hugging Face. CHUNK_RECORDS_DIR = _env_path( "CHUNK_RECORDS_DIR", DATA_DIR / "chunk_records", ) LEGACY_LANCEDB_DIR = _env_path( "LEGACY_LANCEDB_DIR", DATA_DIR / "lancedb", ) UPLOAD_TEMP_DIR = _env_path( "UPLOAD_TEMP_DIR", DATA_DIR / "upload_sessions", ) for folder in ( DOCUMENTS_DIR, DATA_DIR, RUNTIME_DIR, UPLOADS_DIR, LANCEDB_DIR, CHUNK_RECORDS_DIR, UPLOAD_TEMP_DIR, ): folder.mkdir(parents=True, exist_ok=True) # Groq ne sert qu'à reformuler la question et à rédiger la réponse finale. GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip() GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile").strip() QUERY_EXPANSION_ENABLED = os.getenv( "QUERY_EXPANSION_ENABLED", "false" ).lower() in {"1", "true", "yes", "oui"} # Modèles locaux de recherche. EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3").strip() EMBEDDING_DIMENSION = int(os.getenv("EMBEDDING_DIMENSION", "1024")) RERANKER_MODEL = os.getenv( "RERANKER_MODEL", "Alibaba-NLP/gte-multilingual-reranker-base", ).strip() RERANK_ENABLED = os.getenv("RERANK_ENABLED", "true").lower() in { "1", "true", "yes", "oui" } # Profil prudent pour le CPU gratuit Hugging Face (2 vCPU). TORCH_NUM_THREADS = int(os.getenv("TORCH_NUM_THREADS", "2")) EMBED_BATCH_SIZE = int(os.getenv("EMBED_BATCH_SIZE", "2")) RERANK_BATCH_SIZE = int(os.getenv("RERANK_BATCH_SIZE", "1")) PAGE_BATCH_SIZE = int(os.getenv("PAGE_BATCH_SIZE", "4")) CHUNK_WORDS = int(os.getenv("CHUNK_WORDS", "260")) CHUNK_OVERLAP_WORDS = int(os.getenv("CHUNK_OVERLAP_WORDS", "60")) # Recherche à haut rappel, puis reranking précis. DENSE_CANDIDATES = int(os.getenv("DENSE_CANDIDATES", "100")) TEXT_CANDIDATES = int(os.getenv("TEXT_CANDIDATES", "100")) RRF_K = int(os.getenv("RRF_K", "60")) RERANK_POOL = int(os.getenv("RERANK_POOL", "15")) FINAL_SOURCES = int(os.getenv("FINAL_SOURCES", "5")) NEIGHBOR_PAGES = int(os.getenv("NEIGHBOR_PAGES", "1")) MAX_CONTEXT_CHARS = int(os.getenv("MAX_CONTEXT_CHARS", "48000")) # OCR désactivé par défaut dans l'image Docker légère. OCR_ENABLED = os.getenv("OCR_ENABLED", "false").lower() in { "1", "true", "yes", "oui" } OCR_LANGUAGE = os.getenv("OCR_LANGUAGE", "fra+eng").strip() OCR_DPI = int(os.getenv("OCR_DPI", "150")) OCR_MIN_TEXT_CHARS = int(os.getenv("OCR_MIN_TEXT_CHARS", "40")) TESSERACT_CMD = os.getenv("TESSERACT_CMD", "").strip() EXTRACT_TABLES = os.getenv("EXTRACT_TABLES", "false").lower() in { "1", "true", "yes", "oui" } AUTO_OPTIMIZE = os.getenv("AUTO_OPTIMIZE", "true").lower() in { "1", "true", "yes", "oui" } HOST = os.getenv("HOST", "0.0.0.0") PORT = int(os.getenv("PORT", "7860")) MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "4096")) SUPPORTED_EXTENSIONS = {".pdf", ".txt", ".md", ".docx"} # Upload navigateur massif et reprenable. UPLOAD_CHUNK_MB = int(os.getenv("UPLOAD_CHUNK_MB", "8")) UPLOAD_CHUNK_BYTES = UPLOAD_CHUNK_MB * 1024 * 1024 UPLOAD_RESERVE_GB = float(os.getenv("UPLOAD_RESERVE_GB", "0.25")) UPLOAD_RESERVE_BYTES = int(UPLOAD_RESERVE_GB * 1024**3) # Authentification et rôles Hugging Face OAuth. SPACE_HOST = os.getenv("SPACE_HOST", "").strip() SPACE_ID = os.getenv("SPACE_ID", "").strip() SPACE_REPO_NAME = os.getenv("SPACE_REPO_NAME", "").strip() IS_HF_SPACE = bool(SPACE_HOST or SPACE_ID or SPACE_REPO_NAME) OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID", "").strip() OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET", "").strip() OAUTH_SCOPES = os.getenv("OAUTH_SCOPES", "openid profile").strip() OPENID_PROVIDER_URL = os.getenv("OPENID_PROVIDER_URL", "").strip() OAUTH_ENABLED = bool( OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and OPENID_PROVIDER_URL ) OAUTH_REDIRECT_URI = ( f"https://{SPACE_HOST}/auth/callback" if SPACE_HOST else os.getenv("OAUTH_REDIRECT_URI", "").strip() ) # Ce secret signe le cookie de session Flask. Sur le Space, ajoute-le comme # Secret. En dernier recours, le secret OAuth stable permet au Space de # démarrer, mais un FLASK_SECRET_KEY distinct reste recommandé. FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "").strip() if not FLASK_SECRET_KEY: FLASK_SECRET_KEY = OAUTH_CLIENT_SECRET or "atlas-local-development-only" ATLAS_AUTHORIZED_ORG = os.getenv( "ATLAS_AUTHORIZED_ORG", "atlas-rag-team", ).strip() ATLAS_ADMIN_USERS = _env_users( "ATLAS_ADMIN_USERS", "Raphael07", ) ATLAS_MANAGER_USERS = _env_users( "ATLAS_MANAGER_USERS", "SupAIL3,divinetonduangu123", ) ATLAS_LOCAL_USER = os.getenv( "ATLAS_LOCAL_USER", "Raphael07", ).strip() or "Raphael07" ALLOW_LOCAL_AUTH = os.getenv( "ALLOW_LOCAL_AUTH", "true", ).lower() in {"1", "true", "yes", "oui"}