Spaces:
Running
Running
| """Central configuration, loaded from environment / .env. | |
| Everything has a safe default so the app runs fully offline with zero config. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from functools import lru_cache | |
| from pathlib import Path | |
| BACKEND_DIR = Path(__file__).resolve().parent.parent # .../backend | |
| REPO_ROOT = BACKEND_DIR.parent | |
| # Make the repo-root `prototype/` package importable (only the composition layer | |
| # imports it, and only in prototype mode — real code never depends on prototype). | |
| import sys as _sys # noqa: E402 | |
| if str(REPO_ROOT) not in _sys.path: | |
| _sys.path.insert(0, str(REPO_ROOT)) | |
| try: # load .env deterministically (independent of cwd): backend/.env then repo/.env | |
| from dotenv import load_dotenv | |
| load_dotenv(BACKEND_DIR / ".env") | |
| load_dotenv(REPO_ROOT / ".env") | |
| load_dotenv() # finally, anything discoverable from cwd (no override) | |
| except Exception: # pragma: no cover - dotenv optional | |
| pass | |
| # On serverless hosts (Vercel) the bundle is read-only except /tmp. | |
| IS_SERVERLESS = bool(os.getenv("VERCEL") or os.getenv("AWS_LAMBDA_FUNCTION_NAME")) | |
| def _writable_dir() -> Path: | |
| """A directory we can actually write to (metrics DB, eval reports).""" | |
| if IS_SERVERLESS: | |
| d = Path("/tmp/aperture") | |
| else: | |
| d = BACKEND_DIR / "data" | |
| try: | |
| d.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| d = Path("/tmp/aperture") | |
| d.mkdir(parents=True, exist_ok=True) | |
| return d | |
| def _bool(name: str, default: bool) -> bool: | |
| val = os.getenv(name) | |
| if val is None: | |
| return default | |
| return val.strip().lower() in {"1", "true", "yes", "on"} | |
| def _float(name: str, default: float) -> float: | |
| try: | |
| return float(os.getenv(name, default)) | |
| except (TypeError, ValueError): | |
| return default | |
| class Settings: | |
| """Plain object (not pydantic-settings) to avoid a hard dependency in tests.""" | |
| def __init__(self) -> None: | |
| # --- hosted models --- | |
| self.anthropic_api_key: str | None = os.getenv("ANTHROPIC_API_KEY") or None | |
| self.gemini_api_key: str | None = ( | |
| os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or None | |
| ) | |
| self.anthropic_model_smart = os.getenv("ANTHROPIC_MODEL_SMART", "claude-sonnet-4-6") | |
| self.anthropic_model_cheap = os.getenv( | |
| "ANTHROPIC_MODEL_CHEAP", "claude-haiku-4-5-20251001" | |
| ) | |
| self.gemini_model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") | |
| # --- local model --- | |
| self.ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") | |
| self.local_model = os.getenv("LOCAL_MODEL", "gemma2:2b") | |
| self.local_backend = os.getenv("LOCAL_BACKEND", "ollama") # ollama | transformers | |
| # --- deployment mode --------------------------------------------------- | |
| # real = production-first: real OCR / RAG / LLM / browser only. | |
| # prototype = re-enable simulated/offline fallbacks (mock LLM, simulated | |
| # browser, sidecar OCR) from the prototype/ package. | |
| # Serverless (Vercel) defaults to prototype so the public demo still runs. | |
| self.mode = os.getenv("APERTURE_MODE") or ("prototype" if IS_SERVERLESS else "real") | |
| self.prototype_mode = self.mode == "prototype" | |
| # --- routing / caching --- | |
| self.routing_policy = os.getenv("ROUTING_POLICY", "auto") # auto|cheap|smart|offline | |
| self.enable_prompt_cache = _bool("ENABLE_PROMPT_CACHE", True) | |
| self.enable_semantic_cache = _bool("ENABLE_SEMANTIC_CACHE", True) | |
| self.semantic_cache_threshold = _float("SEMANTIC_CACHE_THRESHOLD", 0.92) | |
| # --- pipeline --- | |
| self.hitl_confidence_threshold = _float("HITL_CONFIDENCE_THRESHOLD", 0.85) | |
| # --- storage (always writable; /tmp on serverless) --- | |
| self.writable_dir = _writable_dir() | |
| db = os.getenv("METRICS_DB_PATH") | |
| if db: | |
| self.metrics_db_path = Path(db) if Path(db).is_absolute() else BACKEND_DIR / db | |
| else: | |
| self.metrics_db_path = self.writable_dir / "metrics.db" | |
| try: | |
| self.metrics_db_path.parent.mkdir(parents=True, exist_ok=True) | |
| except OSError: | |
| self.metrics_db_path = self.writable_dir / "metrics.db" | |
| # eval report: committed copy (read) + writable copy (fresh runs) | |
| self.eval_report_committed = BACKEND_DIR / "evals" / "report.json" | |
| self.eval_report_writable = self.writable_dir / "report.json" | |
| # --- auth (HTTP Basic on /api/*) --- | |
| self.auth_user = os.getenv("APERTURE_USER", "demo") | |
| self.auth_pass = os.getenv("APERTURE_PASS", "aperture2026") | |
| # admin role (observability dashboard, prompt mgmt). Defaults to same creds. | |
| self.admin_user = os.getenv("APERTURE_ADMIN_USER", self.auth_user) | |
| self.admin_pass = os.getenv("APERTURE_ADMIN_PASS", self.auth_pass) | |
| # --- OCR backends ----------------------------------------------------- | |
| # selection: auto | minicpm | cohere | llamaparse | tesseract | easyocr | sidecar | |
| self.ocr_backend = os.getenv("OCR_BACKEND", "auto") | |
| # Option 1 — MiniCPM-V-4.6 via OpenAI-compatible server (vLLM / llama.cpp) | |
| self.minicpm_base_url = os.getenv("MINICPM_BASE_URL", "").rstrip("/") or None | |
| self.minicpm_api_key = os.getenv("MINICPM_API_KEY") or None | |
| self.minicpm_model = os.getenv("MINICPM_MODEL", "MiniCPM-V-4.6-Instruct") | |
| # Option 2 — Cohere vision/OCR model from HuggingFace (transformers) | |
| self.cohere_ocr_model = os.getenv("COHERE_OCR_MODEL", "CohereLabs/aya-vision-8b") | |
| # Option 3 — LlamaParse (LlamaCloud) | |
| self.llama_cloud_api_key = os.getenv("LLAMA_CLOUD_API_KEY") or None | |
| self.llamaparse_result_type = os.getenv("LLAMAPARSE_RESULT_TYPE", "markdown") | |
| # --- model labs (all ≤32B params — "small models") -------------------- | |
| # OpenBMB (MiniCPM family) — text/vision reasoning + OCR (via MINICPM_* above) | |
| self.openbmb_model = os.getenv("OPENBMB_MODEL", self.minicpm_model) | |
| # OpenBMB MiniCPM3-4B — text reasoning / NLQ→SQL / summarization (ERP DocIQ + fine-tune target) | |
| self.openbmb_reasoner_model = os.getenv("OPENBMB_REASONER_MODEL", "MiniCPM3-4B") | |
| # Black Forest Labs (FLUX) — image GENERATION for synthetic test documents | |
| self.bfl_api_key = os.getenv("BFL_API_KEY") or None | |
| self.bfl_model = os.getenv("BFL_MODEL", "flux-dev") # api: flux-dev | flux-pro-1.1 | flux-schnell | |
| # Cohere hosted API (in addition to the local HF Aya-Vision backend above) | |
| self.cohere_api_key = os.getenv("COHERE_API_KEY") or None | |
| # --- databases -------------------------------------------------------- | |
| appdb = os.getenv("APP_DB_PATH") | |
| self.app_db_path = ( | |
| (Path(appdb) if Path(appdb).is_absolute() else BACKEND_DIR / appdb) | |
| if appdb else self.writable_dir / "aperture.db" | |
| ) | |
| ragdb = os.getenv("RAG_DB_PATH") | |
| self.rag_db_path = ( | |
| (Path(ragdb) if Path(ragdb).is_absolute() else BACKEND_DIR / ragdb) | |
| if ragdb else self.writable_dir / "rag.db" | |
| ) | |
| erpdb = os.getenv("ERP_DB_PATH") | |
| self.erp_db_path = ( | |
| (Path(erpdb) if Path(erpdb).is_absolute() else BACKEND_DIR / erpdb) | |
| if erpdb else self.writable_dir / "erp.db" | |
| ) | |
| # --- browser --- | |
| self.playwright_headless = _bool("PLAYWRIGHT_HEADLESS", True) | |
| self.demo_portal_url = os.getenv("DEMO_PORTAL_URL", "/portal") | |
| # --- paths --- | |
| self.samples_dir = REPO_ROOT / "samples" | |
| self.evals_dataset_dir = BACKEND_DIR / "evals" / "datasets" | |
| self.demo_portal_dir = REPO_ROOT / "demo-portal" | |
| self.frontend_dist_dir = REPO_ROOT / "frontend" / "dist" | |
| # --- capability detection ------------------------------------------------- | |
| def has_anthropic(self) -> bool: | |
| return bool(self.anthropic_api_key) and _module_available("anthropic") | |
| def has_gemini(self) -> bool: | |
| return bool(self.gemini_api_key) and _module_available("google.generativeai") | |
| def has_local(self) -> bool: | |
| # We can't cheaply ping ollama here; LocalProvider does a lazy health check. | |
| # Treat "configured" as available and let the provider degrade if not. | |
| return _bool("ENABLE_LOCAL_MODEL", False) or _module_available("transformers") | |
| def _module_available(name: str) -> bool: | |
| import importlib.util | |
| try: | |
| return importlib.util.find_spec(name) is not None | |
| except (ImportError, ValueError): | |
| return False | |
| def get_settings() -> Settings: | |
| return Settings() | |