TradeFlowAI / src /config.py
github-actions[bot]
Automated deployment from GitHub Actions: c077d743be852092402bf29515950ab5874e2735
1cf88ff
Raw
History Blame Contribute Delete
11.3 kB
"""
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()