grant-radar / src /analyzer /config.py
Riley
perf: Optimize latency and reduce token usage across QA and search services
f71cdac
Raw
History Blame Contribute Delete
7.09 kB
"""
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}")