generated from kgod/ai-review-template
feat: add resume agent MVP
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_ENV_FILE = BACKEND_ROOT / ".env"
|
||||
|
||||
|
||||
def _as_bool(value: str | None, default: bool) -> bool:
|
||||
if value is None:
|
||||
return default
|
||||
normalized = value.strip().lower()
|
||||
if normalized in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if normalized in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
raise ValueError(f"Invalid boolean configuration value: {value!r}")
|
||||
|
||||
|
||||
def _as_int(name: str, value: str | None, default: int) -> int:
|
||||
if value is None:
|
||||
return default
|
||||
parsed = int(value)
|
||||
if parsed < 0:
|
||||
raise ValueError(f"{name} must be non-negative")
|
||||
return parsed
|
||||
|
||||
|
||||
def _as_float(name: str, value: str | None, default: float) -> float:
|
||||
if value is None:
|
||||
return default
|
||||
parsed = float(value)
|
||||
if parsed <= 0:
|
||||
raise ValueError(f"{name} must be positive")
|
||||
return parsed
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Settings:
|
||||
"""Runtime settings with the API key deliberately hidden from repr output."""
|
||||
|
||||
llm_provider: str = "auto"
|
||||
openai_api_key: str | None = field(default=None, repr=False)
|
||||
openai_base_url: str | None = None
|
||||
openai_model: str = "gpt-4o-mini"
|
||||
openai_timeout_seconds: float = 30.0
|
||||
openai_max_retries: int = 2
|
||||
structured_output_retries: int = 1
|
||||
structured_output_mode: str = "json_schema"
|
||||
fallback_to_rules: bool = True
|
||||
|
||||
@property
|
||||
def use_openai(self) -> bool:
|
||||
if self.llm_provider == "openai":
|
||||
if not self.openai_api_key:
|
||||
raise ValueError("OPENAI_API_KEY is required when LLM provider is openai")
|
||||
return True
|
||||
if self.llm_provider == "rule":
|
||||
return False
|
||||
if self.llm_provider != "auto":
|
||||
raise ValueError("RESUME_AGENT_LLM_PROVIDER must be auto, openai, or rule")
|
||||
return bool(self.openai_api_key)
|
||||
|
||||
|
||||
def load_settings(env_file: str | Path | None = None) -> Settings:
|
||||
selected_file = Path(
|
||||
env_file or os.getenv("RESUME_AGENT_ENV_FILE", str(DEFAULT_ENV_FILE))
|
||||
)
|
||||
load_dotenv(selected_file, override=False)
|
||||
mode = os.getenv("OPENAI_STRUCTURED_OUTPUT_MODE", "json_schema").strip().lower()
|
||||
if mode not in {"json_schema", "json_object"}:
|
||||
raise ValueError(
|
||||
"OPENAI_STRUCTURED_OUTPUT_MODE must be json_schema or json_object"
|
||||
)
|
||||
provider = os.getenv("RESUME_AGENT_LLM_PROVIDER", "auto").strip().lower()
|
||||
settings = Settings(
|
||||
llm_provider=provider,
|
||||
openai_api_key=os.getenv("OPENAI_API_KEY") or None,
|
||||
openai_base_url=os.getenv("OPENAI_BASE_URL") or None,
|
||||
openai_model=os.getenv("OPENAI_MODEL", "gpt-4o-mini").strip(),
|
||||
openai_timeout_seconds=_as_float(
|
||||
"OPENAI_TIMEOUT_SECONDS", os.getenv("OPENAI_TIMEOUT_SECONDS"), 30.0
|
||||
),
|
||||
openai_max_retries=_as_int(
|
||||
"OPENAI_MAX_RETRIES", os.getenv("OPENAI_MAX_RETRIES"), 2
|
||||
),
|
||||
structured_output_retries=_as_int(
|
||||
"OPENAI_STRUCTURED_OUTPUT_RETRIES",
|
||||
os.getenv("OPENAI_STRUCTURED_OUTPUT_RETRIES"),
|
||||
1,
|
||||
),
|
||||
structured_output_mode=mode,
|
||||
fallback_to_rules=_as_bool(
|
||||
os.getenv("RESUME_AGENT_LLM_FALLBACK_TO_RULES"), True
|
||||
),
|
||||
)
|
||||
if not settings.openai_model:
|
||||
raise ValueError("OPENAI_MODEL cannot be blank")
|
||||
return settings
|
||||
Reference in New Issue
Block a user