Spaces:
Sleeping
Sleeping
File size: 11,299 Bytes
db4ba8d dd9584b db4ba8d 621eb30 db4ba8d 621eb30 db4ba8d 621eb30 db4ba8d dd9584b db4ba8d dd9584b db4ba8d 1cf88ff db4ba8d dd9584b db4ba8d dd9584b db4ba8d ae08068 dd9584b ae08068 db4ba8d 2951010 db4ba8d dd9584b db4ba8d dd9584b db4ba8d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """
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 = ""
@property
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 = ""
@field_validator("CORS_ORIGINS", mode="before")
@classmethod
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
@field_validator("DEBUG", mode="before")
@classmethod
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
@lru_cache
def get_settings() -> Settings:
return Settings() # type: ignore[call-arg]
settings = get_settings()
|