from typing import Literal from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str = "ClientFlow" env: str = "dev" openrouter_api_key: str openrouter_model: str = "qwen/qwen3-30b-a3b" openrouter_url: str = "https://openrouter.ai/api/v1/chat/completions" # Optional OpenAI Responses API used by the Phase 1 safe email reply agent. # When disabled or not configured, ClientFlow keeps the existing deterministic/OpenRouter flow. openai_api_key: str = "" openai_responses_url: str = "https://api.openai.com/v1/responses" openai_vector_store_id: str = "" # ClientFlow requer PostgreSQL. SQLite não suporta JSONB, UUID, # TIMESTAMPTZ nem os índices usados pelo schema core. database_url: str clientflow_persist: bool = True clientflow_webhook_secret: str = "" clientflow_admin_auth_mode: Literal["proxy", "token", "local"] = "proxy" clientflow_admin_token: str = "" # UI warning for cases where the original opportunity contact and the # linked fiscal customer look different. Disabled by default because many # legitimate contacts use abbreviated names or personal contacts for a # company/fiscal customer. Enable only if this warning proves useful. clientflow_customer_mismatch_warning_enabled: bool = False # Automatic CONFIRM_DELIVERY tasks are intentionally disabled. The normal # cadence starts directly with the commercial follow-up; an operator may # still schedule a manual delivery check from the opportunity page. confirm_delivery_automation_enabled: bool = False chatwoot_write_enabled: bool = False chatwoot_base_url: str = "" chatwoot_public_url: str = "" chatwoot_account_id: str = "" chatwoot_api_token: str = "" # Reply assistant: deterministic templates + optional business-aware LLM wording. clientflow_reply_llm_enabled: bool = False # When true and LLM is enabled, deterministic intent rules are used only as guardrails. clientflow_reply_llm_first_enabled: bool = True clientflow_reply_llm_model: str = "" clientflow_reply_llm_temperature: float = 0.2 clientflow_reply_llm_max_tokens: int = 700 clientflow_reply_llm_timeout_seconds: int = 20 clientflow_reply_llm_max_context_chars: int = 5000 # Phase 1: safe draft-only email reply agent backed by OpenAI file_search. clientflow_email_reply_agent_enabled: bool = False clientflow_email_reply_agent_model: str = "gpt-4.1-mini" clientflow_email_reply_agent_timeout_seconds: int = 30 clientflow_email_reply_agent_max_results: int = 6 clientflow_email_reply_agent_prompt_version: str = "blif-email-agent-phase1-20260613" # External operational systems odoo_enabled: bool = False odoo_base_url: str = "" odoo_public_url: str = "" odoo_db: str = "" odoo_username: str = "" odoo_api_key: str = "" odoo_api_mode: str = "xmlrpc" jasmin_enabled: bool = False jasmin_base_url: str = "" jasmin_public_url: str = "" jasmin_account: str = "" jasmin_subscription: str = "" jasmin_client_id: str = "" jasmin_client_secret: str = "" jasmin_token_url: str = "https://identity.primaverabss.com/connect/token" jasmin_scope: str = "application" jasmin_company_key: str = "" jasmin_quotation_type: str = "ORC" jasmin_quotation_serie: str = "" jasmin_default_price_list: str = "03" jasmin_default_payment_method: str = "TRA" jasmin_default_payment_term: str = "00" jasmin_default_delivery_term: str = "TRANSP" jasmin_default_currency: str = "EUR" jasmin_default_country: str = "PT" jasmin_default_customer_group: str = "02" jasmin_default_party_tax_schema: str = "CONTINENTE" jasmin_default_unit: str = "UN" jasmin_default_item_tax_schema: str = "NORMAL" jasmin_default_sales_item: str = "" packlink_enabled: bool = False packlink_base_url: str = "" packlink_public_url: str = "" packlink_api_key: str = "" packlink_default_service_id: str = "20571" packlink_default_service: str = "Paq 24" packlink_default_carrier: str = "Correos Express" packlink_source: str = "PRO" packlink_platform: str = "PRO" packlink_platform_country: str = "UN" # External company/contact lookup API used by the fiscal enrichment worker. # Example base URL: http://127.0.0.1:8000 from informa_pipeline_api. external_company_lookup_enabled: bool = False external_company_lookup_base_url: str = "" external_company_lookup_api_key: str = "" external_company_lookup_timeout: int = 10 external_company_lookup_auto_threshold: float = 95.0 # LLM-assisted identity extraction from email body/signature. email_identity_extraction_enabled: bool = True email_identity_extraction_use_llm: bool = True email_identity_llm_model: str = "" email_identity_llm_fallback_model: str = "" email_identity_llm_timeout_seconds: int = 20 email_identity_llm_max_body_chars: int = 3500 max_tokens: int = 2200 temperature: float = 0.0 model_config = SettingsConfigDict( env_file=".env", env_prefix="", extra="ignore", ) settings = Settings() def is_production_like_env() -> bool: return str(settings.env or "").strip().lower() in {"prod", "production", "staging"} def validate_admin_auth_settings() -> None: if settings.clientflow_admin_auth_mode == "local" and is_production_like_env(): raise RuntimeError( "CLIENTFLOW_ADMIN_AUTH_MODE=local não é permitido em prod/production/staging." ) if ( settings.clientflow_admin_auth_mode == "token" and not str(settings.clientflow_admin_token or "").strip() ): raise RuntimeError( "CLIENTFLOW_ADMIN_TOKEN é obrigatório quando CLIENTFLOW_ADMIN_AUTH_MODE=token." ) validate_admin_auth_settings() if ( is_production_like_env() and settings.external_company_lookup_enabled and str(settings.external_company_lookup_base_url or "").strip() and not str(settings.external_company_lookup_api_key or "").strip() ): raise RuntimeError( "EXTERNAL_COMPANY_LOOKUP_API_KEY é obrigatória quando o lookup externo está ativo em produção." ) if settings.database_url.strip().lower().startswith("sqlite"): raise RuntimeError( "ClientFlow requer PostgreSQL. Defina DATABASE_URL com postgresql+psycopg://..." )