generated from kgod/ai-review-template
228 lines
9.0 KiB
Python
228 lines
9.0 KiB
Python
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"
|
|
embedding_provider: str = "tei"
|
|
embedding_base_url: str = "http://127.0.0.1:8081"
|
|
embedding_model: str = "BAAI/bge-m3"
|
|
embedding_dimensions: int = 1024
|
|
embedding_timeout_seconds: float = 30.0
|
|
embedding_batch_size: int = 32
|
|
openai_timeout_seconds: float = 30.0
|
|
openai_max_retries: int = 2
|
|
light_opt_rate_limit: int = 20
|
|
light_opt_rate_window_seconds: float = 3600.0
|
|
structured_output_retries: int = 1
|
|
structured_output_mode: str = "json_schema"
|
|
fallback_to_rules: bool = True
|
|
intent_router_mode: str = "off"
|
|
intent_model: str | None = None
|
|
knowledge_admin_token: str | None = field(default=None, repr=False)
|
|
database_url: str | None = field(default=None, repr=False)
|
|
offerpai_auth_base_url: str = "https://test.offerpai.com.cn"
|
|
offerpai_auth_timeout_seconds: float = 8.0
|
|
offerpai_auth_required: bool = True
|
|
offerpai_resume_api_base_url: str = "https://test.offerpai.com.cn/api"
|
|
offerpai_resume_timeout_seconds: float = 8.0
|
|
deep_max_questions: int = 6
|
|
deep_min_questions: int = 2
|
|
deep_gap_threshold: float = 5.0
|
|
|
|
@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 == "volcengine":
|
|
if not self.openai_api_key:
|
|
raise ValueError(
|
|
"VOLCENGINE_API_KEY is required when LLM provider is volcengine"
|
|
)
|
|
return True
|
|
if self.llm_provider == "rule":
|
|
return False
|
|
if self.llm_provider != "auto":
|
|
raise ValueError(
|
|
"RESUME_AGENT_LLM_PROVIDER must be auto, openai, volcengine, 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()
|
|
if provider == "volcengine":
|
|
llm_api_key = os.getenv("VOLCENGINE_API_KEY") or None
|
|
llm_base_url = os.getenv("VOLCENGINE_BASE_URL") or None
|
|
llm_model = os.getenv("VOLCENGINE_MODEL", "").strip()
|
|
else:
|
|
llm_api_key = os.getenv("OPENAI_API_KEY") or None
|
|
llm_base_url = os.getenv("OPENAI_BASE_URL") or None
|
|
llm_model = os.getenv("OPENAI_MODEL", "gpt-4o-mini").strip()
|
|
intent_mode = os.getenv("RESUME_AGENT_INTENT_ROUTER_MODE", "off").strip().lower()
|
|
if intent_mode not in {"off", "shadow", "on"}:
|
|
raise ValueError("RESUME_AGENT_INTENT_ROUTER_MODE must be off, shadow, or on")
|
|
settings = Settings(
|
|
llm_provider=provider,
|
|
openai_api_key=llm_api_key,
|
|
openai_base_url=llm_base_url,
|
|
openai_model=llm_model,
|
|
embedding_provider=os.getenv("EMBEDDING_PROVIDER", "tei").strip().lower(),
|
|
embedding_base_url=os.getenv("EMBEDDING_BASE_URL", "http://127.0.0.1:8081").rstrip("/"),
|
|
embedding_model=os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3").strip(),
|
|
embedding_dimensions=_as_int(
|
|
"EMBEDDING_DIMENSIONS", os.getenv("EMBEDDING_DIMENSIONS"), 1024
|
|
),
|
|
embedding_timeout_seconds=_as_float(
|
|
"EMBEDDING_TIMEOUT_SECONDS", os.getenv("EMBEDDING_TIMEOUT_SECONDS"), 30.0
|
|
),
|
|
embedding_batch_size=_as_int(
|
|
"EMBEDDING_BATCH_SIZE", os.getenv("EMBEDDING_BATCH_SIZE"), 32
|
|
),
|
|
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
|
|
),
|
|
light_opt_rate_limit=_as_int(
|
|
"RESUME_AGENT_LIGHT_OPT_RATE_LIMIT",
|
|
os.getenv("RESUME_AGENT_LIGHT_OPT_RATE_LIMIT"),
|
|
20,
|
|
),
|
|
light_opt_rate_window_seconds=_as_float(
|
|
"RESUME_AGENT_LIGHT_OPT_RATE_WINDOW_SECONDS",
|
|
os.getenv("RESUME_AGENT_LIGHT_OPT_RATE_WINDOW_SECONDS"),
|
|
3600.0,
|
|
),
|
|
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
|
|
),
|
|
intent_router_mode=intent_mode,
|
|
intent_model=os.getenv("RESUME_AGENT_INTENT_MODEL", "").strip() or None,
|
|
knowledge_admin_token=os.getenv("KNOWLEDGE_ADMIN_TOKEN") or None,
|
|
database_url=os.getenv("DATABASE_URL") or None,
|
|
offerpai_auth_base_url=os.getenv(
|
|
"OFFERPAI_AUTH_BASE_URL", "https://test.offerpai.com.cn"
|
|
).rstrip("/"),
|
|
offerpai_auth_timeout_seconds=_as_float(
|
|
"OFFERPAI_AUTH_TIMEOUT_SECONDS",
|
|
os.getenv("OFFERPAI_AUTH_TIMEOUT_SECONDS"),
|
|
8.0,
|
|
),
|
|
offerpai_auth_required=_as_bool(
|
|
os.getenv("OFFERPAI_AUTH_REQUIRED"), True
|
|
),
|
|
offerpai_resume_api_base_url=os.getenv(
|
|
"OFFERPAI_RESUME_API_BASE_URL", "https://test.offerpai.com.cn/api"
|
|
).rstrip("/"),
|
|
offerpai_resume_timeout_seconds=_as_float(
|
|
"OFFERPAI_RESUME_TIMEOUT_SECONDS",
|
|
os.getenv("OFFERPAI_RESUME_TIMEOUT_SECONDS"),
|
|
8.0,
|
|
),
|
|
deep_max_questions=_as_int(
|
|
"RESUME_AGENT_DEEP_MAX_QUESTIONS",
|
|
os.getenv("RESUME_AGENT_DEEP_MAX_QUESTIONS"),
|
|
6,
|
|
),
|
|
deep_min_questions=_as_int(
|
|
"RESUME_AGENT_DEEP_MIN_QUESTIONS",
|
|
os.getenv("RESUME_AGENT_DEEP_MIN_QUESTIONS"),
|
|
2,
|
|
),
|
|
deep_gap_threshold=_as_float(
|
|
"RESUME_AGENT_DEEP_GAP_THRESHOLD",
|
|
os.getenv("RESUME_AGENT_DEEP_GAP_THRESHOLD"),
|
|
5.0,
|
|
),
|
|
)
|
|
if settings.embedding_provider not in {"tei", "openai", "hash"}:
|
|
raise ValueError("EMBEDDING_PROVIDER must be tei, openai, or hash")
|
|
if not settings.embedding_model:
|
|
raise ValueError("EMBEDDING_MODEL cannot be blank")
|
|
if settings.embedding_provider == "tei" and not settings.embedding_base_url:
|
|
raise ValueError("EMBEDDING_BASE_URL cannot be blank when EMBEDDING_PROVIDER is tei")
|
|
if settings.embedding_dimensions < 1:
|
|
raise ValueError("EMBEDDING_DIMENSIONS must be positive")
|
|
if settings.embedding_batch_size < 1:
|
|
raise ValueError("EMBEDDING_BATCH_SIZE must be positive")
|
|
if not settings.offerpai_auth_base_url.startswith(("http://", "https://")):
|
|
raise ValueError("OFFERPAI_AUTH_BASE_URL must start with http:// or https://")
|
|
if not settings.offerpai_resume_api_base_url.startswith(("http://", "https://")):
|
|
raise ValueError(
|
|
"OFFERPAI_RESUME_API_BASE_URL must start with http:// or https://"
|
|
)
|
|
if settings.deep_max_questions < settings.deep_min_questions:
|
|
raise ValueError(
|
|
"RESUME_AGENT_DEEP_MAX_QUESTIONS must be at least "
|
|
"RESUME_AGENT_DEEP_MIN_QUESTIONS"
|
|
)
|
|
if settings.deep_max_questions < 1:
|
|
raise ValueError("RESUME_AGENT_DEEP_MAX_QUESTIONS must be positive")
|
|
if not settings.openai_model:
|
|
model_setting = "VOLCENGINE_MODEL" if provider == "volcengine" else "OPENAI_MODEL"
|
|
raise ValueError(f"{model_setting} cannot be blank")
|
|
return settings
|