File size: 9,580 Bytes
027123c 19bd037 027123c 81e5fe7 079782d 81e5fe7 0e02a0f 0721bb4 3743cfe 027123c d65c41d 027123c 268ef5d b68816f 027123c 079782d b68816f 7124acf 027123c 19bd037 027123c 2ba0613 c7d59cf 2ba0613 f282b15 268ef5d 027123c | 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 | """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()
|