generated from kgod/ai-review-template
feat: initialize resume agent with OfferPai sync
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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
|
||||
Reference in New Issue
Block a user