"""Application configuration, loaded once from the environment.""" import os from dataclasses import dataclass from functools import lru_cache from pathlib import Path from dotenv import load_dotenv load_dotenv() PROJECT_ROOT = Path(__file__).parent DEFAULT_MODEL = "openrouter/free" OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" DEFAULT_TEMPERATURE = 0.0 # Drafting a cover letter is writing, not analysis: a little sampling gives # natural prose. Kept low so it does not start inventing detail. DEFAULT_DRAFTING_TEMPERATURE = 0.4 # The preset actions (ATS keyword tables, detailed reviews) run long, so 2048 # truncated them mid-answer. This leaves room for the profile, job description, # and conversation history while avoiding an unnecessarily large response cap. DEFAULT_MAX_TOKENS = 20000 # Prose skill output gets its own response cap. DEFAULT_SKILL_MAX_TOKENS = 6500 # ATS returns a structured classification for every keyword, so it needs more # output room than the prose skills. DEFAULT_ATS_MAX_TOKENS = 12000 DEFAULT_LANGFUSE_HOST = "https://cloud.langfuse.com" DEFAULT_CHECKPOINT_DB = "checkpoints.sqlite" # Turns of conversation replayed to the model. Older turns stay in the # checkpoint (nothing is deleted) but are not resent. DEFAULT_MAX_HISTORY_TURNS = 10 # Routing and retrieval. ONNX runtime, so no torch dependency. DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2" DEFAULT_RETRIEVAL_TOP_K = 10 class ConfigError(RuntimeError): """Raised when the environment is missing or misconfigured.""" @dataclass(frozen=True) class Settings: openrouter_api_key: str candidate_name: str model: str temperature: float drafting_temperature: float max_tokens: int skill_max_tokens: int ats_max_tokens: int max_history_turns: int embedding_model: str retrieval_top_k: int checkpoint_db: Path langfuse_public_key: str langfuse_secret_key: str langfuse_host: str @property def tracing_enabled(self) -> bool: """Tracing turns itself on only when both Langfuse keys are present.""" return bool(self.langfuse_public_key and self.langfuse_secret_key) def _get_float(name, default): raw = os.getenv(name) if raw is None or not raw.strip(): return default try: return float(raw) except ValueError: raise ConfigError(f"{name} must be a number, got {raw!r}") def _get_int(name, default): raw = os.getenv(name) if raw is None or not raw.strip(): return default try: return int(raw) except ValueError: raise ConfigError(f"{name} must be an integer, got {raw!r}") @lru_cache(maxsize=2) def get_settings(require_api_key=True) -> Settings: api_key = os.getenv("OPENROUTER_API_KEY", "").strip() if require_api_key and not api_key: raise ConfigError("OPENROUTER_API_KEY is not set. Add it to your .env file.") return Settings( openrouter_api_key=api_key, candidate_name=os.getenv("CANDIDATE_NAME", "").strip() or "the candidate", model=os.getenv("OPENROUTER_MODEL", "").strip() or DEFAULT_MODEL, temperature=_get_float("LLM_TEMPERATURE", DEFAULT_TEMPERATURE), drafting_temperature=_get_float("LLM_DRAFTING_TEMPERATURE", DEFAULT_DRAFTING_TEMPERATURE), max_tokens=_get_int("LLM_MAX_TOKENS", DEFAULT_MAX_TOKENS), skill_max_tokens=_get_int("SKILL_MAX_TOKENS", DEFAULT_SKILL_MAX_TOKENS), ats_max_tokens=_get_int("ATS_MAX_TOKENS", DEFAULT_ATS_MAX_TOKENS), max_history_turns=_get_int("MAX_HISTORY_TURNS", DEFAULT_MAX_HISTORY_TURNS), embedding_model=os.getenv("EMBEDDING_MODEL", "").strip() or DEFAULT_EMBEDDING_MODEL, retrieval_top_k=_get_int("RETRIEVAL_TOP_K", DEFAULT_RETRIEVAL_TOP_K), checkpoint_db=PROJECT_ROOT / ( os.getenv("CHECKPOINT_DB", "").strip() or DEFAULT_CHECKPOINT_DB ), langfuse_public_key=os.getenv("LANGFUSE_PUBLIC_KEY", "").strip(), langfuse_secret_key=os.getenv("LANGFUSE_SECRET_KEY", "").strip(), # LANGFUSE_HOST is the SDK's own name; LANGFUSE_BASE_URL is accepted as an alias. langfuse_host=( os.getenv("LANGFUSE_HOST", "").strip() or os.getenv("LANGFUSE_BASE_URL", "").strip() or DEFAULT_LANGFUSE_HOST ), )