"""Settings loader for the micro-trend Gradio app. Loads `settings.json` (same shape as `sample_code/settings.json`) with env overrides, and exposes a typed Settings object. """ from __future__ import annotations import json import os from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Optional DEFAULT_SETTINGS_PATH = Path("settings.json") # Keys mirrored from sample_code/settings.json SETTING_KEYS = { "OPENAI_API_KEY", "GEMINI_API_KEY", "OPENAI_MODEL", "OPENAI_REASONING_EFFORT", "GOOGLE_GENAI_USE_VERTEXAI", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION", } DEFAULT_MODEL = "gpt-5-mini" DEFAULT_REASONING = "medium" @dataclass class Settings: openai_api_key: Optional[str] = None gemini_api_key: Optional[str] = None openai_model: str = DEFAULT_MODEL openai_reasoning_effort: Optional[str] = DEFAULT_REASONING google_genai_use_vertexai: bool = True google_cloud_project: Optional[str] = None google_cloud_location: Optional[str] = None def require_api_keys(self) -> None: """Raise if both providers are missing keys.""" if not self.openai_api_key and not self.gemini_api_key: raise RuntimeError("No API keys set: provide OPENAI_API_KEY and/or GEMINI_API_KEY via env or settings.json") def to_payload(self) -> Dict[str, Any]: """Return a dict useful for client construction/logging.""" return { "openai_model": self.openai_model, "openai_reasoning_effort": self.openai_reasoning_effort, "google_genai_use_vertexai": self.google_genai_use_vertexai, "google_cloud_project": self.google_cloud_project, "google_cloud_location": self.google_cloud_location, } def _coerce_bool(value: Any) -> bool: if isinstance(value, bool): return value if isinstance(value, str): return value.strip().lower() in {"1", "true", "yes", "on"} return bool(value) def _load_json(path: Path) -> Dict[str, Any]: if not path.exists(): return {} return json.loads(path.read_text(encoding="utf-8")) def load_settings(path: Path | None = None) -> Settings: """ Load settings with env overrides. Precedence: env > settings.json > defaults. """ settings_path = path or DEFAULT_SETTINGS_PATH raw = _load_json(settings_path) # Keep only recognized keys raw = {k: v for k, v in raw.items() if k in SETTING_KEYS} def pick(key: str, default: Any = None) -> Any: env_val = os.environ.get(key) return env_val if env_val is not None else raw.get(key, default) return Settings( openai_api_key=pick("OPENAI_API_KEY"), gemini_api_key=pick("GEMINI_API_KEY"), openai_model=pick("OPENAI_MODEL", DEFAULT_MODEL), openai_reasoning_effort=pick("OPENAI_REASONING_EFFORT", DEFAULT_REASONING), google_genai_use_vertexai=_coerce_bool(pick("GOOGLE_GENAI_USE_VERTEXAI", True)), google_cloud_project=pick("GOOGLE_CLOUD_PROJECT"), google_cloud_location=pick("GOOGLE_CLOUD_LOCATION"), )