File size: 3,123 Bytes
2948ced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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"),
    )