|
|
"""Application configuration using Pydantic Settings.""" |
|
|
|
|
|
import logging |
|
|
from typing import Literal |
|
|
|
|
|
import structlog |
|
|
from pydantic import Field |
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict |
|
|
|
|
|
from src.utils.exceptions import ConfigurationError |
|
|
|
|
|
|
|
|
class Settings(BaseSettings): |
|
|
"""Strongly-typed application settings.""" |
|
|
|
|
|
model_config = SettingsConfigDict( |
|
|
env_file=".env", |
|
|
env_file_encoding="utf-8", |
|
|
case_sensitive=False, |
|
|
extra="ignore", |
|
|
) |
|
|
|
|
|
|
|
|
openai_api_key: str | None = Field(default=None, description="OpenAI API key") |
|
|
anthropic_api_key: str | None = Field(default=None, description="Anthropic API key") |
|
|
llm_provider: Literal["openai", "anthropic"] = Field( |
|
|
default="openai", description="Which LLM provider to use" |
|
|
) |
|
|
openai_model: str = Field(default="gpt-4o", description="OpenAI model name") |
|
|
anthropic_model: str = Field( |
|
|
default="claude-3-5-sonnet-20241022", description="Anthropic model" |
|
|
) |
|
|
|
|
|
|
|
|
ncbi_api_key: str | None = Field( |
|
|
default=None, description="NCBI API key for higher rate limits" |
|
|
) |
|
|
|
|
|
|
|
|
max_iterations: int = Field(default=10, ge=1, le=50) |
|
|
search_timeout: int = Field(default=30, description="Seconds to wait for search") |
|
|
|
|
|
|
|
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" |
|
|
|
|
|
def get_api_key(self) -> str: |
|
|
"""Get the API key for the configured provider.""" |
|
|
if self.llm_provider == "openai": |
|
|
if not self.openai_api_key: |
|
|
raise ConfigurationError("OPENAI_API_KEY not set") |
|
|
return self.openai_api_key |
|
|
|
|
|
if self.llm_provider == "anthropic": |
|
|
if not self.anthropic_api_key: |
|
|
raise ConfigurationError("ANTHROPIC_API_KEY not set") |
|
|
return self.anthropic_api_key |
|
|
|
|
|
raise ConfigurationError(f"Unknown LLM provider: {self.llm_provider}") |
|
|
|
|
|
|
|
|
def get_settings() -> Settings: |
|
|
"""Factory function to get settings (allows mocking in tests).""" |
|
|
return Settings() |
|
|
|
|
|
|
|
|
def configure_logging(settings: Settings) -> None: |
|
|
"""Configure structured logging with the configured log level.""" |
|
|
|
|
|
logging.basicConfig( |
|
|
level=getattr(logging, settings.log_level), |
|
|
format="%(message)s", |
|
|
) |
|
|
|
|
|
structlog.configure( |
|
|
processors=[ |
|
|
structlog.stdlib.filter_by_level, |
|
|
structlog.stdlib.add_logger_name, |
|
|
structlog.stdlib.add_log_level, |
|
|
structlog.processors.TimeStamper(fmt="iso"), |
|
|
structlog.processors.JSONRenderer(), |
|
|
], |
|
|
wrapper_class=structlog.stdlib.BoundLogger, |
|
|
context_class=dict, |
|
|
logger_factory=structlog.stdlib.LoggerFactory(), |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
settings = get_settings() |
|
|
|