"""Centralized configuration management using pydantic-settings.""" # import os # from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Application settings loaded from environment variables.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="allow", case_sensitive=False, ) # Feature flags # REMOVED 2026-07-02: the former ENABLE_SLOW_PATH flag is gone. `structured_flow` # now always runs the analytical slow path (Planner -> TaskRunner -> Assembler), so # every structured question persists a report_inputs record (reports no longer # depend on flipping a flag). extra="allow" ignores a stale ENABLE_SLOW_PATH in .env. # DEPRECATED 2026-06-24: the problem_validated gate was removed (the goal is now # user-entered objective + business_questions, no agent validation). This flag no # longer has any effect — the gate call site in ChatHandler is commented out. Kept # to avoid .env churn; remove once no environment references it. enable_gate: bool = Field(alias="enable_gate", default=False) # Skip init_db() (create_all + startup DDL) on boot. TRUE by default post-dedorch # cutover: Go owns the dedorch schema, so Python (consumer-only role) must NOT run # init_db — its ALTER/index DDL on Go-owned tables fails with InsufficientPrivilege # ("must be owner of table rooms"). Set to false only for a local Python-owned DB. skip_init_db: bool = Field(alias="SKIP_INIT_DB", default=True) # Database postgres_connstring: str # Redis redis_url: str redis_prefix: str = "dataeyond-agent-service_" # Azure OpenAI - GPT-5.4-mini (map to .env names with double underscores) azureai_api_key_54m: str = Field(alias="azureai__api_key__54m", default="") azureai_endpoint_url_54m: str = Field(alias="azureai__endpoint__url__54m", default="") azureai_deployment_name_54m: str = Field(alias="azureai__deployment__name__54m", default="") azureai_api_version_54m: str = Field(alias="azureai__api__version__54m", default="") # Azure OpenAI - GPT-6 Luna (trial 2026-11-03), same shape as the quad above. azureai_api_key_6l: str = Field(alias="azureai__api_key__6l", default="") azureai_endpoint_url_6l: str = Field(alias="azureai__endpoint__url__6l", default="") azureai_deployment_name_6l: str = Field(alias="azureai__deployment__name__6l", default="") azureai_api_version_6l: str = Field(alias="azureai__api__version__6l", default="") # Which quad the chat/planner/report LLM calls use: "54m" (default) or "6l". # Switching back is this one variable and a restart — the fallback if a model # trial does not work out. Any other value reads as "54m". llm_profile: str = Field(alias="LLM_PROFILE", default="54m") # Azure OpenAI - GPT-5.4-nano (knowledge-extraction pipeline; all four # branches route here — see KNOWLEDGE_PIPELINE_TODO.md D3) azureai_api_key_54n: str = Field(alias="azureai__api_key__54n", default="") azureai_endpoint_url_54n: str = Field(alias="azureai__endpoint__url__54n", default="") azureai_deployment_name_54n: str = Field(alias="azureai__deployment__name__54n", default="") azureai_api_version_54n: str = Field(alias="azureai__api__version__54n", default="") # Azure OpenAI - Embeddings azureai_api_key_embedding: str = Field(alias="azureai__api_key__embedding", default="") azureai_endpoint_url_embedding: str = Field(alias="azureai__endpoint__url__embedding", default="") azureai_deployment_name_embedding: str = Field(alias="azureai__deployment__name__embedding", default="") azureai_api_version_embedding: str = Field(alias="azureai__api__version__embedding", default="") # Azure Document Intelligence azureai_docintel_endpoint: str = Field(alias="azureai__docintel__endpoint", default="") azureai_docintel_key: str = Field(alias="azureai__docintel__key", default="") # Azure Blob Storage azureai_blob_sas: str = Field(alias="azureai__blob__sas", default="") azureai_container_endpoint: str = Field(alias="azureai__container__endpoint", default="") azureai_container_name: str = Field(alias="azureai__container__name", default="") azureai_container_account_name: str = Field(alias="azureai__container__account__name", default="") # Object storage provider toggle. Mirrors the Go data plane's `storage.provider` # (see Orchestrator config: azure_blob | supabase_s3). Blank falls back to azure_blob. # Tabular query execution reads the processed Parquet from whichever backend is active. storage_provider: str = Field(alias="STORAGE_PROVIDER", default="azure_blob") # Supabase S3-compatible object storage (active when storage_provider == "supabase_s3"). # BRIDGE (Option C): lets Python read the same bucket Go writes to, until Go exposes a # source-data endpoint (see docs/proposal_go_source_data_endpoint.md) — then this client # is removed and Python stops touching storage directly. supabase_s3_bucket: str = Field(alias="SUPABASE_S3_BUCKET", default="") supabase_s3_endpoint: str = Field(alias="SUPABASE_S3_ENDPOINT", default="") supabase_s3_region: str = Field(alias="SUPABASE_S3_REGION", default="") supabase_s3_access_key_id: str = Field(alias="SUPABASE_S3_ACCESS_KEY_ID", default="") supabase_s3_secret_access_key: str = Field(alias="SUPABASE_S3_SECRET_ACCESS_KEY", default="") # Mistral OCR — the knowledge-parsing primary backend (no GPU). Read here so the # key reaches a deployed HuggingFace Space the same way every other credential # does, instead of a bare os.environ lookup that a Space's secret injection can # miss. The standalone parser (`py -m src.knowledge_parsing.run`) still falls # back to the environment / repo .env — see src/knowledge_parsing/mistral_ocr.py. mistral_api_key: str = Field(alias="MISTRAL_API_KEY", default="") # Which OCR parser `POST /api/v2/document/process` uses (added 2026-09-24): # "paddleocr" (default) or "mistral". Server-side only — the request carries no # parser choice. The offline knowledge pipeline is NOT governed by this; its # default stays in src/knowledge_parsing/config.py. document_parser_backend: str = Field(alias="DOCUMENT_PARSER_BACKEND", default="paddleocr") # PaddleOCR-VL hosted job API (Baidu AI Studio). Names follow the official SDK. paddleocr_access_token: str = Field(alias="PADDLEOCR_ACCESS_TOKEN", default="") paddleocr_job_url: str = Field( alias="PADDLEOCR_JOB_URL", default="https://paddleocr.aistudio-app.com/api/v2/ocr/jobs" ) paddleocr_model: str = Field(alias="PADDLEOCR_MODEL", default="PaddleOCR-VL-1.6") # Langfuse LANGFUSE_PUBLIC_KEY: str LANGFUSE_SECRET_KEY: str LANGFUSE_HOST: str # MongoDB (for users - existing) # emarcal_mongo_endpoint_url: str = Field(alias="emarcal__mongo__endpoint__url", default="") # emarcal_buma_mongo_dbname: str = Field(alias="emarcal__buma__mongo__dbname", default="") # JWT (for users - existing) emarcal_jwt_secret_key: str = Field(alias="emarcal__jwt__secret_key", default="") emarcal_jwt_algorithm: str = Field(alias="emarcal__jwt__algorithm", default="HS256") # Bcrypt salt (for users - existing) emarcal_bcrypt_salt: str = Field(alias="emarcal__bcrypt__salt", default="") # DB credential encryption (Fernet key for user-registered database creds) dataeyond_db_credential_key: str = Field( alias="dataeyond__db__credential__key" ) # Shared service secret for the F-2 gate — UNWIRED 2026-07-27 (DEV_PLAN #37). The # gate is no longer mounted (see main.py), so this value is read by nothing. Kept # commented rather than deleted so the whole feature restores in one place. If a # deployment still has `dataeyond__service__secret` in its .env it is simply # ignored (extra="allow" on Settings). Restore alongside the main.py dependency. # dataeyond_service_secret: str = Field( # alias="dataeyond__service__secret", default="" # ) # The active chat-LLM quad, chosen by `llm_profile`. @property def _llm_suffix(self) -> str: return "6l" if self.llm_profile.strip().lower() == "6l" else "54m" @property def llm_api_key(self) -> str: return getattr(self, f"azureai_api_key_{self._llm_suffix}") @property def llm_endpoint_url(self) -> str: return getattr(self, f"azureai_endpoint_url_{self._llm_suffix}") @property def llm_deployment_name(self) -> str: return getattr(self, f"azureai_deployment_name_{self._llm_suffix}") @property def llm_api_version(self) -> str: return getattr(self, f"azureai_api_version_{self._llm_suffix}") # GPT-6 Luna on chat/completions (checked 2026-11-03): function tools — every # with_structured_output call — are refused unless reasoning_effort is "none", and # temperature accepts only 0 and 1. None leaves the 5.4-mini request unchanged. @property def llm_reasoning_effort(self) -> str | None: return "none" if self._llm_suffix == "6l" else None def llm_temperature(self, wanted: float) -> float: """`wanted`, or for Luna the nearer of the two values it accepts.""" if self._llm_suffix != "6l": return wanted return 0.0 if wanted < 0.5 else 1.0 # Singleton instance settings = Settings()