Spaces:
Sleeping
Sleeping
github-actions[bot]
Automated deployment from GitHub Actions: c077d743be852092402bf29515950ab5874e2735
1cf88ff | """ | |
| TradeFlow AI — Application Configuration (T-004) | |
| SDD §2.1 + PRD §22 Invariant: No bare os.getenv(). | |
| ALL environment variables are validated here via pydantic-settings. | |
| This is the SINGLE source of truth for configuration. | |
| """ | |
| from __future__ import annotations | |
| from functools import lru_cache | |
| from typing import Literal | |
| from pydantic import AnyHttpUrl, Field, SecretStr, field_validator | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict( | |
| env_file=".env", | |
| env_file_encoding="utf-8", | |
| case_sensitive=False, | |
| extra="ignore", | |
| ) | |
| # ── Application ─────────────────────────────────────────────────────────── | |
| ENVIRONMENT: Literal["development", "staging", "production"] = "development" | |
| DEBUG: bool = False | |
| SECRET_KEY: SecretStr = Field(..., min_length=32) | |
| CORS_ORIGINS: list[str] = ["*"] | |
| # ── Database ────────────────────────────────────────────────────────────── | |
| DATABASE_URL: str # asyncpg connection string e.g. postgresql+asyncpg://... | |
| # ── Supabase ────────────────────────────────────────────────────────────── | |
| SUPABASE_URL: str | |
| SUPABASE_ANON_KEY: str | |
| SUPABASE_SERVICE_KEY: SecretStr | |
| SUPABASE_JWT_SECRET: SecretStr | |
| DISABLE_AUTH: bool = False | |
| # ── Keycloak 26 — SOLE auth provider (Invariant #4) ─────────────────────── | |
| KEYCLOAK_SERVER_URL: str = "" | |
| KEYCLOAK_REALM: str = "tradeflow" | |
| KEYCLOAK_CLIENT_ID: str = "tradeflow-api" | |
| KEYCLOAK_CLIENT_SECRET: SecretStr = "" # type: ignore[assignment] | |
| KEYCLOAK_ISSUER: str = "" | |
| def KEYCLOAK_JWKS_URL(self) -> str: | |
| base = self.KEYCLOAK_SERVER_URL.rstrip("/") | |
| return f"{base}/realms/{self.KEYCLOAK_REALM}/protocol/openid-connect/certs" | |
| # ── Redis 8 Standalone ──────────────────────────────────────────────────── | |
| REDIS_URL: str = "redis://localhost:6379/0" | |
| REDIS_CELERY_DB: int = 0 | |
| REDIS_CACHE_DB: int = 1 | |
| # ── AI Inference Services (SDD §2.3–2.6) ───────────────────────────────── | |
| CLOUD_LLM_ONLY: bool = False # Bypass heavy local models and use Gemini API instead | |
| ENABLE_DUAL_OCR: bool = False # Use both Surya and PaddleOCR for fallback/validation | |
| OCR_FALLBACK_TRIGGER_QUALITY: float = 0.85 # Fallback threshold | |
| OCR_FALLBACK_TRIGGER_CONFIDENCE: float = 0.80 | |
| SURYA_INFERENCE_URL: AnyHttpUrl = "http://surya-svc:8001" # Agent A | |
| OLM_INFERENCE_URL: AnyHttpUrl = "http://olm-inference:8000" # Agent D | |
| PADDLEOCR_SVC_URL: AnyHttpUrl = "http://paddleocr-svc:8002" # Agent B | |
| MINERU_SVC_URL: AnyHttpUrl = "http://mineru-svc:8003" # Preprocessor | |
| OLM_BASE_MODEL: str = "allenai/olmOCR-2-7B-1025" | |
| OLM_LORA_ADAPTER: str = "muhammadghiffari/olm-ocr-cipl-v1" | |
| HF_TOKEN: SecretStr = "" # type: ignore[assignment] | |
| # ── Azure Document Intelligence — Agent C ───────────────────────────────── | |
| AZURE_DI_ENDPOINT: str | None = None | |
| AZURE_DI_KEY: SecretStr = "" # type: ignore[assignment] | |
| AZURE_DI_MODEL_ID: str = "prebuilt-read" | |
| AZURE_DI_FREE_LIMIT: int = 5000 # Pages/month on F0 tier (Invariant #9) | |
| # ── CEISA (Simulator in dev, real endpoint in prod) ─────────────────────── | |
| CEISA_BASE_URL: AnyHttpUrl = "http://simulator:8006" | |
| CEISA_CLIENT_ID: str = "" | |
| CEISA_CLIENT_SECRET: SecretStr = "" # type: ignore[assignment] | |
| CEISA_REQUEST_TIMEOUT_SECONDS: int = 30 | |
| CEISA_POLL_INTERVAL_SECONDS: int = 30 | |
| CEISA_AES_KEY: SecretStr = "" # type: ignore[assignment] # AES-256-GCM key for payload encryption (base64) | |
| # ── Blockchain ──────────────────────────────────────────────────────────── | |
| ENABLE_BLOCKCHAIN: bool = True | |
| OPERATOR_WALLET_PRIVATE_KEY: SecretStr = "" # type: ignore[assignment] # Never log! | |
| POLYGON_RPC_URL: str = "https://rpc-amoy.polygon.technology" | |
| CONTRACT_ADDRESS: str = "" | |
| PINATA_JWT: SecretStr = "" # type: ignore[assignment] | |
| POLYGON_MAX_FEE_GWEI: int = 80 | |
| POLYGON_MAX_PRIORITY_FEE_GWEI: int = 3 | |
| POLYGON_ANCHOR_GAS_LIMIT: int = 250_000 | |
| # ── Notifications ───────────────────────────────────────────────────────── | |
| RESEND_API_KEY: SecretStr = "" # type: ignore[assignment] | |
| WHATSAPP_TOKEN: SecretStr = "" # type: ignore[assignment] | |
| WHATSAPP_PHONE_NUMBER_ID: str = "" | |
| NOTIFICATION_EMAIL_FROM: str = "noreply@tradeflow.ai" | |
| # ── ChromaDB ────────────────────────────────────────────────────────────── | |
| CHROMADB_HOST: str = "localhost" | |
| CHROMADB_PORT: int = 8000 | |
| # ── AI / LLM ───────────────────────────────────────────────────────────── | |
| USE_LOCAL_LLM: bool = False | |
| LOCAL_LLM_MODEL: str = "qwen2.5:7b" | |
| DIGITAL_PDF_SKIP_LLM: bool = True | |
| LLM_EXTRACTION_TIMEOUT_SECONDS: int = 25 | |
| OLLAMA_BASE_URL: str = "http://host.docker.internal:11434/v1" | |
| GEMINI_API_KEY: SecretStr = Field(..., description="Google Gemini API key") | |
| GEMINI_MODEL_PRIMARY: str = "gemini-3.5-flash" | |
| GEMINI_MODEL_FALLBACK: str = "gemini-3.1-flash-lite" | |
| OPENAI_API_KEY: SecretStr = "" # type: ignore[assignment] | |
| EMBEDDING_MODEL: str = "text-embedding-3-small" | |
| # LangSmith tracing | |
| LANGCHAIN_TRACING_V2: bool = True | |
| LANGCHAIN_PROJECT: str = "tradeflow-ai" | |
| LANGCHAIN_API_KEY: SecretStr = "" # type: ignore[assignment] | |
| # ── Feature Flags ───────────────────────────────────────────────────────── | |
| ENABLE_SURYA_AGENT: bool = True | |
| ENABLE_AZURE_DI_AGENT: bool = True | |
| ENABLE_VESSEL_VALIDATION: bool = True | |
| ENABLE_BLOCKCHAIN: bool = True # type: ignore[assignment] — redeclared intentionally | |
| ENABLE_INSW_CHECK: bool = True | |
| ENABLE_NOTIFICATIONS_WHATSAPP: bool = False | |
| ENABLE_ADAPTIVE_LEARNING: bool = True | |
| ENABLE_AI_COPILOT: bool = True | |
| ENABLE_HS_RAG: bool = True | |
| ENABLE_REJECTION_PREDICTION: bool = True | |
| ENABLE_STATUS_POLLING: bool = True | |
| ENABLE_MARITIME_DATA_FEATURES: bool = True | |
| DETERMINISTIC_E2E: bool = False # Used in extract.py to swap LLM for deterministic mock | |
| # ── Thresholds ──────────────────────────────────────────────────────────── | |
| OCR_MAX_RENDERED_PAGES: int = 10 | |
| OCR_MAX_LLM_PAGES: int = 5 | |
| OCR_FAST_PATH_QUALITY_THRESHOLD: float = 0.95 | |
| OCR_PDF_TEXT_MIN_CHARS: int = 250 | |
| OCR_PDF_TEXT_MIN_CHARS_PER_PAGE: int = 80 | |
| OCR_RECONCILIATION_DISAGREEMENT_THRESHOLD: float = 0.20 | |
| LLM_CONFIDENCE_REVIEW_THRESHOLD: float = 0.70 | |
| CRS_MIN_SUBMIT_THRESHOLD: int = 55 | |
| HS_CONFIDENCE_RAG_THRESHOLD: float = 0.75 | |
| XGB_MIN_SAMPLES_FOR_MODEL: int = 500 | |
| MAX_RESUBMIT_ATTEMPTS: int = 5 | |
| REJECTION_RISK_BLOCK_THRESHOLD: float = 0.70 | |
| # ── Validation rules ────────────────────────────────────────────────────── | |
| VALIDATION_RULES_PATH: str = "packages/db/validation_rules.json" | |
| CARRIER_PROFILES_PATH: str = "packages/db/carrier_profiles.json" | |
| XGBOOST_MODEL_PATH: str = "models/rejection_predictor.json" | |
| # ── Storage ─────────────────────────────────────────────────────────────── | |
| STORAGE_BACKEND: Literal["supabase", "minio"] = "supabase" | |
| STORAGE_BUCKET_NAME: str = "tradeflow-docs" | |
| MINIO_ENDPOINT: str = "localhost:9000" | |
| MINIO_ACCESS_KEY: str = "minioadmin" | |
| MINIO_SECRET_KEY: SecretStr = "" # type: ignore[assignment] | |
| # ── Adaptive learning / drift ───────────────────────────────────────────── | |
| RETRAIN_MIN_NEW_SAMPLES: int = 100 | |
| RETRAIN_MIN_TOTAL_SAMPLES: int = 500 | |
| DRIFT_LOOKBACK_DAYS: int = 30 | |
| DRIFT_CORRECTION_THRESHOLD: int = 50 | |
| # Celery Configuration | |
| CELERY_BROKER_URL: str = "redis://redis:6379/0" | |
| CELERY_RESULT_BACKEND: str = "redis://redis:6379/0" | |
| CELERY_TASK_SOFT_TIME_LIMIT: int = 1800 # 30 minutes for slow OCR models | |
| CELERY_TASK_TIME_LIMIT: int = 1900 | |
| RUN_OCR_IN_API_BACKGROUND: bool = False | |
| # ── Observability ───────────────────────────────────────────────────────── | |
| SENTRY_DSN: str = "" | |
| POSTHOG_API_KEY: str = "" | |
| OTEL_ENABLED: bool = False | |
| OTEL_EXPORTER_OTLP_ENDPOINT: str = "" | |
| def parse_cors_origins(cls, v: str | list[str]) -> list[str]: | |
| if isinstance(v, str): | |
| return [origin.strip() for origin in v.split(",")] | |
| return v | |
| def parse_debug(cls, v: bool | str) -> bool: | |
| if isinstance(v, str) and v.lower() in {"release", "prod", "production"}: | |
| return False | |
| return bool(v) | |
| class CeleryConfig: | |
| """Celery configuration — separate class for Celery's config_from_object.""" | |
| task_serializer = "json" | |
| result_serializer = "json" | |
| accept_content = ["json"] | |
| timezone = "Asia/Jakarta" | |
| enable_utc = True | |
| task_soft_time_limit = 600 | |
| task_time_limit = 700 | |
| task_acks_late = True # Ack only after successful completion (NFR-016) | |
| worker_prefetch_multiplier = 1 # One task at a time per worker | |
| def get_settings() -> Settings: | |
| return Settings() # type: ignore[call-arg] | |
| settings = get_settings() | |