File size: 7,089 Bytes
59ebe66
2ae7490
59ebe66
2ae7490
 
59ebe66
2ae7490
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59ebe66
 
2ae7490
 
 
 
59ebe66
 
2ae7490
59ebe66
 
 
2ae7490
59ebe66
2ae7490
59ebe66
 
f71cdac
59ebe66
 
 
2ae7490
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59ebe66
2ae7490
59ebe66
2ae7490
59ebe66
2ae7490
 
 
 
 
 
f71cdac
2ae7490
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f71cdac
2ae7490
f71cdac
59ebe66
 
2ae7490
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59ebe66
2ae7490
59ebe66
 
 
 
 
2ae7490
 
 
 
 
 
 
59ebe66
2ae7490
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
Centralized configuration for Grant Analyst.

All runtime configuration loaded from environment variables with sensible defaults.
Supports OpenAI as the primary LLM provider with provider validation.

ENV VARS
--------
# Environment
ENV=dev|staging|prod                 # default: dev

# Database
MONGO_URI=                           # MongoDB connection string
MONGO_DB=grant_analyst               # Database name
REDIS_URL=                           # Redis connection URL (optional)

# LLM Provider
LLM_PROVIDER=openai                  # Only OpenAI supported
LLM_MODEL=gpt-5-mini                 # Fallback model
LLM_MODEL_ROUTER=gpt-5-nano          # Fast routing/classification
LLM_MODEL_QA=gpt-5-mini              # QA and analysis
LLM_MODEL_TRANSLATE=gpt-5-mini       # Translations

# Auth
OPENAI_API_KEY=                      # Required for OpenAI

# Security
ALLOWED_ORIGINS=                     # Comma-separated CORS origins

# Behavior
MAX_QUERY_CHARS=4000                 # Max query length
LLM_TEMPERATURE=0.2                  # 0..2
LLM_MAX_OUTPUT_TOKENS=800            # model-dependent cap
LLM_TIMEOUT_S=30                     # HTTP timeout (seconds)
LLM_DISABLE=0                        # 1 disables external calls

# Logging
LOG_LEVEL=INFO                       # DEBUG|INFO|WARNING|ERROR
"""

from __future__ import annotations

import os
from functools import lru_cache
from typing import List, Optional


class Settings:
    """Centralized settings loaded from environment variables."""

    # Environment
    ENV: str

    # Database
    MONGO_URI: Optional[str]
    MONGO_DB: str
    REDIS_URL: Optional[str]

    # LLM Configuration
    LLM_PROVIDER: str
    LLM_MODEL_ROUTER: str
    LLM_MODEL_QA: str
    LLM_MODEL_TRANSLATE: str

    # Auth
    OPENAI_API_KEY: Optional[str]
    ANTHROPIC_API_KEY: Optional[str]

    # Security
    ALLOWED_ORIGINS: List[str]
    MAX_QUERY_CHARS: int

    # LLM Behavior
    TEMPERATURE: float
    MAX_OUTPUT_TOKENS: int
    TIMEOUT_S: float
    DISABLE_LLM: bool

    # Logging
    LOG_LEVEL: str

    def __init__(self) -> None:
        """Load all settings from environment variables."""
        self.ENV = os.getenv("ENV", "dev")

        # Database
        self.MONGO_URI = os.getenv("MONGO_URI")
        self.MONGO_DB = os.getenv("MONGO_DB", os.getenv("MONGO_DB_NAME", "grant_analyst"))
        self.REDIS_URL = os.getenv("REDIS_URL")

        # LLM Provider & Models
        self.LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai")
        base_model = os.getenv("LLM_MODEL", "gpt-5-mini")
        self.LLM_MODEL_ROUTER = os.getenv("LLM_MODEL_ROUTER", os.getenv("LLM_MODEL", "gpt-5-nano"))
        self.LLM_MODEL_QA = os.getenv("LLM_MODEL_QA", base_model)
        self.LLM_MODEL_TRANSLATE = os.getenv("LLM_MODEL_TRANSLATE", base_model)

        # Auth
        self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
        self.ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")

        # Security - CORS
        allowed = os.getenv("ALLOWED_ORIGINS", "")
        if allowed:
            self.ALLOWED_ORIGINS = [o.strip() for o in allowed.split(",") if o.strip()]
        else:
            # Dev-safe default; override in prod
            self.ALLOWED_ORIGINS = ["*"] if self.ENV == "dev" else []

        # Security - Limits
        self.MAX_QUERY_CHARS = int(os.getenv("MAX_QUERY_CHARS", "4000"))

        # LLM Behavior
        self.TEMPERATURE = float(os.getenv("LLM_TEMPERATURE", "0.2"))
        self.MAX_OUTPUT_TOKENS = int(os.getenv("LLM_MAX_OUTPUT_TOKENS", "800"))
        self.TIMEOUT_S = float(os.getenv("LLM_TIMEOUT_S", "30"))
        self.DISABLE_LLM = os.getenv("LLM_DISABLE", "0") == "1"

        # Logging
        self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

        # Validate critical settings
        self._validate()

    def _validate(self) -> None:
        """Validate critical configuration."""
        # Only OpenAI is fully supported
        if self.LLM_PROVIDER != "openai":
            raise ValueError(
                f"Unsupported LLM_PROVIDER: {self.LLM_PROVIDER}. "
                "Only 'openai' is currently supported."
            )

        # Auth validation (skip if LLM is disabled)
        if not self.DISABLE_LLM:
            if self.LLM_PROVIDER == "openai" and not self.OPENAI_API_KEY:
                raise ValueError("OPENAI_API_KEY is required when LLM_PROVIDER=openai")
            if self.LLM_PROVIDER == "anthropic" and not self.ANTHROPIC_API_KEY:
                raise ValueError("ANTHROPIC_API_KEY is required when LLM_PROVIDER=anthropic")

        # Range validation
        if not (0.0 <= self.TEMPERATURE <= 2.0):
            raise ValueError("LLM_TEMPERATURE must be between 0 and 2")
        if self.MAX_OUTPUT_TOKENS <= 0:
            raise ValueError("LLM_MAX_OUTPUT_TOKENS must be > 0")
        if self.TIMEOUT_S <= 0:
            raise ValueError("LLM_TIMEOUT_S must be > 0")
        if self.MAX_QUERY_CHARS <= 0:
            raise ValueError("MAX_QUERY_CHARS must be > 0")

        # Security warning for production
        if self.ENV != "dev" and not self.ALLOWED_ORIGINS:
            import logging

            logging.warning(
                "ALLOWED_ORIGINS not set in production environment. "
                "CORS will be disabled for security."
            )


@lru_cache(maxsize=1)
def get_settings() -> Settings:
    """Get cached settings instance (singleton pattern)."""
    return Settings()


# Legacy compatibility: load_config() for existing code
def load_config():
    """
    Legacy function for backward compatibility.
    Returns a dict-like object with config values.

    DEPRECATED: Use get_settings() instead.
    """
    import warnings

    warnings.warn(
        "load_config() is deprecated. Use get_settings() instead.", DeprecationWarning, stacklevel=2
    )

    settings = get_settings()

    # Create a simple namespace object that acts like the old Config dataclass
    class LegacyConfig:
        def __init__(self, s: Settings):
            self.provider = s.LLM_PROVIDER
            self.model = s.LLM_MODEL_QA
            self.openai_api_key = s.OPENAI_API_KEY
            self.anthropic_api_key = s.ANTHROPIC_API_KEY
            self.model_router = s.LLM_MODEL_ROUTER
            self.model_translator = s.LLM_MODEL_TRANSLATE
            self.model_analyzer = s.LLM_MODEL_QA  # Use QA model for analysis
            self.temperature = s.TEMPERATURE
            self.max_output_tokens = s.MAX_OUTPUT_TOKENS
            self.timeout_s = s.TIMEOUT_S
            self.disable_llm = s.DISABLE_LLM
            self.log_level = s.LOG_LEVEL

    return LegacyConfig(settings)


# Quick self-test when run directly
if __name__ == "__main__":
    try:
        s = get_settings()
        print("Settings loaded successfully:")
        print(f"  ENV: {s.ENV}")
        print(f"  LLM_PROVIDER: {s.LLM_PROVIDER}")
        print(f"  LLM_MODEL_QA: {s.LLM_MODEL_QA}")
        print(f"  MONGO_DB: {s.MONGO_DB}")
        print(f"  ALLOWED_ORIGINS: {s.ALLOWED_ORIGINS}")
    except Exception as e:
        print(f"Config error: {e}")