MyPal / app /core /config.py
KhaledSalehKL1's picture
Deploy 9XAIPal: Gradio+FastAPI app, backend, React build
1086e43 verified
Raw
History Blame Contribute Delete
10 kB
"""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"
@property
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}"
)
@property
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"
@property
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
@property
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
@property
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
@property
def effective_celery_broker_url(self) -> str:
return self.celery_broker_url or self.redis_url
@property
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()