Spaces:
Runtime error
Runtime error
| """Application settings loaded from environment variables.""" | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict( | |
| env_file=".env", | |
| env_file_encoding="utf-8", | |
| extra="ignore", | |
| ) | |
| # Application | |
| app_name: str = "9XAIPal" | |
| debug: bool = False | |
| # PostgreSQL | |
| postgres_host: str = "localhost" | |
| postgres_port: int = 5432 | |
| postgres_db: str = "9xaipal" | |
| postgres_user: str = "9xaipal" | |
| postgres_password: str = "9xaipal_dev_password" | |
| def database_url(self) -> str: | |
| return ( | |
| f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}" | |
| f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" | |
| ) | |
| def database_url_sync(self) -> str: | |
| return ( | |
| f"postgresql://{self.postgres_user}:{self.postgres_password}" | |
| f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" | |
| ) | |
| # Storage | |
| storage_root: str = "app/storage" | |
| # MinerU (installed CLI for PDF extraction). MinerU 3.x ships the `mineru` | |
| # binary; the legacy `magic-pdf` 0.x package is abandoned. | |
| mineru_binary: str = "mineru" | |
| # OCR language hint for the pipeline backend. | |
| mineru_lang: str = "en" | |
| # When mineru isn't installed, allow degraded PyMuPDF text-only fallback. | |
| # Disabled by default so a missing extractor fails loudly instead of silently | |
| # producing low-quality output with no OCR/tables/math. | |
| allow_pymupdf_fallback: bool = False | |
| # Hard wall-clock timeout (seconds) for a single MinerU subprocess. A large | |
| # book (e.g. a 700-page PDF) through the full pipeline on CPU-only hardware | |
| # can take hours, so this defaults high. Lower it to fail fast on small docs. | |
| mineru_timeout_sec: int = 14400 # 4 hours | |
| # ββ LLM provider ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Which API answers questions. "auto" (default): use Ollama when it is | |
| # reachable at OLLAMA_BASE_URL, otherwise fall back to the first cloud | |
| # provider below with an API key set (openai β anthropic β gemini β xai β | |
| # deepseek); if neither exists, requests fail with instructions to add an | |
| # API key or an Ollama connection. Set explicitly to pin one backend: | |
| # "ollama", "openai" (GPT), "anthropic" (Claude), "gemini" (Google), | |
| # "xai" (Grok), "deepseek", or "custom" (any OpenAI-compatible endpoint). | |
| llm_provider: str = "auto" | |
| # Generic key, used when LLM_PROVIDER is pinned explicitly. The | |
| # per-provider keys below also work in pinned mode and win when both set. | |
| llm_api_key: str = "" | |
| # Override the provider's default API base URL (required for "custom", | |
| # optional otherwise β e.g. an Azure/OpenRouter/proxy endpoint). | |
| llm_base_url: str = "" | |
| # Per-provider API keys β in auto mode the first non-empty one (in the | |
| # order above) is used when Ollama is unreachable. | |
| openai_api_key: str = "" | |
| anthropic_api_key: str = "" | |
| gemini_api_key: str = "" | |
| xai_api_key: str = "" | |
| deepseek_api_key: str = "" | |
| # Chat model used when each cloud provider is active. CHAT_MODEL / | |
| # VLM_MODEL / CLASSIFIER_MODEL stay reserved for Ollama (and "custom"), | |
| # so switching backends never sends an Ollama tag to a cloud API. | |
| openai_chat_model: str = "gpt-4o" | |
| anthropic_chat_model: str = "claude-sonnet-4-6" | |
| gemini_chat_model: str = "gemini-2.5-flash" | |
| xai_chat_model: str = "grok-4" | |
| # Note: DeepSeek models have no vision support β figure images can't be | |
| # described when DeepSeek is the active provider (captions still work). | |
| deepseek_chat_model: str = "deepseek-chat" | |
| # ββ Cloud thinking / reasoning mode βββββββββββββββββββββββββββββββββββββ | |
| # When True, sends ``reasoning_effort: "medium"`` to OpenAI-compatible | |
| # chat-completions endpoints for reasoning models (o1, o3-mini, o4-mini, | |
| # etc.). Only affects providers/models that support it; silently ignored | |
| # for Anthropic, Gemini, xAI, DeepSeek, Ollama, and non-reasoning models. | |
| # Make sure your active chat model is a reasoning model before enabling. | |
| cloud_thinking_mode: bool = False | |
| # ββ Embedding provider ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # "auto" (default): Ollama when reachable, else OPENAI_API_KEY, else | |
| # GEMINI_API_KEY β only OpenAI and Gemini offer embedding APIs (Anthropic/ | |
| # xAI/DeepSeek don't). Pin to "ollama", "openai", "gemini", or "custom" | |
| # (OpenAI-compatible /embeddings endpoint) to force one. Key/base-url fall | |
| # back to the llm_* values when left empty. | |
| embedding_provider: str = "auto" | |
| embedding_api_key: str = "" | |
| embedding_base_url: str = "" | |
| # Embedding model used when each cloud provider is active. EMBEDDING_MODEL | |
| # stays reserved for Ollama (and "custom"). | |
| openai_embedding_model: str = "text-embedding-3-small" | |
| gemini_embedding_model: str = "gemini-embedding-001" | |
| # Ollama (local default backend or remote API; model names live in .env) | |
| ollama_base_url: str = "http://localhost:11434" | |
| # Optional API key for hosted/protected Ollama endpoints (leave empty for local). | |
| ollama_api_key: str = "" | |
| chat_model: str = "gemma4:26b" | |
| # Vision model for figure descriptions / image questions. Empty = reuse | |
| # chat_model (set it only when a separate multimodal model should handle | |
| # vision, e.g. a smaller VLM). | |
| vlm_model: str = "" | |
| embedding_model: str = "qwen3-embedding" | |
| def effective_vlm_model(self) -> str: | |
| return self.vlm_model or self.chat_model | |
| # ββ Latency tuning ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Small, fast model used ONLY for cheap classification (router + guardrail). | |
| # Leave empty to reuse chat_model. Pointing this at a 1β3B model (e.g. | |
| # "llama3.2:3b", "gemma2:2b") removes two big-model calls from the critical | |
| # path of every question β usually the single biggest /ask speedup. | |
| classifier_model: str = "" | |
| # How long Ollama keeps a model resident after a call. Without this the big | |
| # chat model is unloaded between requests and every question pays a cold | |
| # reload. "-1" = keep forever, "30m" = 30 minutes, "0" = unload immediately. | |
| ollama_keep_alive: str = "30m" | |
| # Cap the answer length so generation can't run away on slow hardware. | |
| # 0 = uncapped (model decides). Classification calls are capped separately. | |
| chat_num_predict: int = 0 | |
| # Skip the LLM topic-guardrail when the user is reading a paper. Paper Q&A is | |
| # in-scope by definition, so this removes a whole model call per question. | |
| guardrail_skip_in_paper: bool = True | |
| def effective_classifier_model(self) -> str: | |
| return self.classifier_model or self.chat_model | |
| # LOCAL context window size (number of chunks on each side of the current one) | |
| local_context_window: int = 3 # Increased from 2 for better "see surrounding" experience | |
| # Stored embedding dimension. Embeddings larger than this are truncated and | |
| # re-normalized (valid for MRL-trained models: qwen3-embedding, | |
| # text-embedding-3-*, gemini-embedding); smaller ones are zero-padded. | |
| # Keep β€ 2000: pgvector's HNSW index has a hard 2000-dim limit, and without | |
| # the index every search is a brute-force scan of all embeddings. | |
| # Changing this triggers an automatic re-embed of the library on next start. | |
| vector_dimension: int = 1024 | |
| # SearXNG | |
| searxng_url: str = "http://localhost:8080" | |
| # Upload limits | |
| max_upload_size_mb: int = 100 | |
| # Max characters of a chunk's text sent to the embedder. Ollama's | |
| # /api/embed hard-400s when inputs exceed the model context window (dense | |
| # tables tokenize heavily β ~3000 chars is a safe ceiling for local | |
| # models). Cloud embedders have larger windows; raise this accordingly. | |
| embed_max_chars: int = 3000 | |
| # ββ Security ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Comma-separated list of allowed browser origins. Add your LAN address | |
| # (e.g. "http://192.168.1.50:5173") when serving the dev frontend to other | |
| # machines. Irrelevant in single-port SPA mode (same origin, no CORS). | |
| cors_origins: str = "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173" | |
| # Per-client-IP request ceiling across all /api routes. Generous enough for | |
| # the UI's polling, low enough to blunt scripted abuse. 0 disables. | |
| rate_limit_per_minute: int = 300 | |
| def cors_origin_list(self) -> list[str]: | |
| return [o.strip() for o in self.cors_origins.split(",") if o.strip()] | |
| # Celery / Redis | |
| redis_url: str = "redis://localhost:6379/0" | |
| celery_broker_url: str | None = None | |
| celery_result_backend: str | None = None | |
| def effective_celery_broker_url(self) -> str: | |
| return self.celery_broker_url or self.redis_url | |
| def effective_celery_result_backend(self) -> str: | |
| return self.celery_result_backend or self.redis_url | |
| # Concurrency tuning for "my machine = server" with multiple simultaneous users. | |
| # These control SQLAlchemy async + sync pool sizes. Increase on a beefy machine | |
| # with many concurrent /ask or ingestion jobs. Decrease for very low-RAM setups. | |
| db_pool_size: int = 10 | |
| db_max_overflow: int = 15 | |
| settings = Settings() | |