apps/whatsapp_control_api/app/core/config.py
text
from __future__ import annotations
import os
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
def _to_bool(value: str | None, default: bool = False) -> bool:
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _resolve_sqlite_path(database_url: str) -> Path:
prefix = "sqlite:///"
if not database_url.startswith(prefix):
raise ValueError("Only sqlite:/// DATABASE_URL is supported in this MVP.")
raw_path = database_url[len(prefix) :]
path = Path(raw_path)
if not path.is_absolute():
path = Path.cwd() / path
return path
@dataclass(slots=True)
class Settings:
app_env: str
app_host: str
app_port: int
log_level: str
meta_app_id: str
meta_app_secret: str
meta_business_id: str
meta_waba_id: str
meta_phone_number_id: str
meta_access_token: str
meta_verify_token: str
meta_graph_api_version: str
whatsapp_webhook_url: str
webhook_signature_required: bool
database_url: str
database_path: Path
hermes_command: str
hermes_args: str
hermes_workspace_root: Path
hermes_projects_root: Path
task_logs_dir: Path
task_artifacts_dir: Path
gateway_registry_tasks_dir: Path
hermes_task_timeout_seconds: int
task_progress_throttle_seconds: int
whatsapp_allowed_numbers: set[str]
outbound_rate_limit_per_minute: int
approval_code_ttl_seconds: int
max_outbound_text_length: int
chat_enabled: bool
chat_provider: str
chat_base_url: str
chat_model: str
chat_api_key: str
chat_temperature: float
chat_max_tokens: int
web_search_enabled: bool
web_search_provider: str
web_search_api_key: str
web_search_base_url: str
web_search_max_results: int
web_search_timeout_seconds: float
dashboard_admin_token: str
gateway_api_token: str
@lru_cache(maxsize=1)
def get_settings() -> Settings:
database_url = os.getenv("DATABASE_URL", "sqlite:///./data/control_layer.db")
database_path = _resolve_sqlite_path(database_url)
task_logs_dir = Path(os.getenv("TASK_LOGS_DIR", "./data/tasks"))
task_artifacts_dir = Path(os.getenv("TASK_ARTIFACTS_DIR", "./data/artifacts"))
gateway_registry_tasks_dir = Path(
os.getenv("HERMES_GATEWAY_REGISTRY_TASKS_DIR", "/root/hermes-projects/.hermes-registry/tasks")
)
if not task_logs_dir.is_absolute():
task_logs_dir = Path.cwd() / task_logs_dir
if not task_artifacts_dir.is_absolute():
task_artifacts_dir = Path.cwd() / task_artifacts_dir
if not gateway_registry_tasks_dir.is_absolute():
gateway_registry_tasks_dir = Path.cwd() / gateway_registry_tasks_dir
allowed_numbers = {
"".join(ch for ch in item if ch.isdigit())
for item in os.getenv("WHATSAPP_ALLOWED_NUMBERS", "").split(",")
if item.strip()
}
return Settings(
app_env=os.getenv("APP_ENV", "production"),
app_host=os.getenv("APP_HOST", "0.0.0.0"),
app_port=int(os.getenv("APP_PORT", "8040")),
log_level=os.getenv("LOG_LEVEL", "INFO").upper(),
meta_app_id=os.getenv("META_APP_ID", "").strip(),
meta_app_secret=os.getenv("META_APP_SECRET", "").strip(),
meta_business_id=os.getenv("META_BUSINESS_ID", "").strip(),
meta_waba_id=os.getenv("META_WABA_ID", "").strip(),
meta_phone_number_id=os.getenv("META_PHONE_NUMBER_ID", "").strip(),
meta_access_token=os.getenv("META_ACCESS_TOKEN", "").strip(),
meta_verify_token=os.getenv("META_VERIFY_TOKEN", "").strip(),
meta_graph_api_version=os.getenv("META_GRAPH_API_VERSION", "v23.0").strip(),
whatsapp_webhook_url=os.getenv("WHATSAPP_WEBHOOK_URL", "").strip(),
webhook_signature_required=_to_bool(os.getenv("WEBHOOK_SIGNATURE_REQUIRED"), True),
database_url=database_url,
database_path=database_path,
hermes_command=os.getenv("HERMES_COMMAND", "/usr/local/bin/hermes").strip(),
hermes_args=os.getenv("HERMES_ARGS", "").strip(),
hermes_workspace_root=Path(os.getenv("HERMES_WORKSPACE_ROOT", "/root")),
hermes_projects_root=Path(os.getenv("HERMES_PROJECTS_ROOT", "/root/hermes-projects")),
task_logs_dir=task_logs_dir,
task_artifacts_dir=task_artifacts_dir,
gateway_registry_tasks_dir=gateway_registry_tasks_dir,
hermes_task_timeout_seconds=int(os.getenv("HERMES_TASK_TIMEOUT_SECONDS", "3600")),
task_progress_throttle_seconds=int(os.getenv("TASK_PROGRESS_THROTTLE_SECONDS", "20")),
whatsapp_allowed_numbers=allowed_numbers,
outbound_rate_limit_per_minute=int(os.getenv("OUTBOUND_RATE_LIMIT_PER_MINUTE", "20")),
approval_code_ttl_seconds=int(os.getenv("APPROVAL_CODE_TTL_SECONDS", "900")),
max_outbound_text_length=int(os.getenv("MAX_OUTBOUND_TEXT_LENGTH", "4096")),
chat_enabled=_to_bool(os.getenv("CHAT_ENABLED"), True),
chat_provider=os.getenv("CHAT_PROVIDER", "local").strip().lower(),
chat_base_url=os.getenv("CHAT_BASE_URL", "https://openrouter.ai/api/v1").strip().rstrip("/"),
chat_model=os.getenv("CHAT_MODEL", "").strip(),
chat_api_key=os.getenv("CHAT_API_KEY", "").strip(),
chat_temperature=float(os.getenv("CHAT_TEMPERATURE", "0.4")),
chat_max_tokens=int(os.getenv("CHAT_MAX_TOKENS", "450")),
web_search_enabled=_to_bool(os.getenv("WEB_SEARCH_ENABLED"), True),
web_search_provider=os.getenv("WEB_SEARCH_PROVIDER", "bing").strip().lower(),
web_search_api_key=os.getenv("WEB_SEARCH_API_KEY", "").strip(),
web_search_base_url=os.getenv("WEB_SEARCH_BASE_URL", "").strip().rstrip("/"),
web_search_max_results=int(os.getenv("WEB_SEARCH_MAX_RESULTS", "5")),
web_search_timeout_seconds=float(os.getenv("WEB_SEARCH_TIMEOUT_SECONDS", "12")),
dashboard_admin_token=os.getenv("DASHBOARD_ADMIN_TOKEN", "").strip(),
gateway_api_token=os.getenv("HERMES_GATEWAY_API_TOKEN", os.getenv("DASHBOARD_ADMIN_TOKEN", "")).strip(),
)