Spaces:
Sleeping
Sleeping
File size: 7,405 Bytes
c687f2b 43c3b77 c687f2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | """Centralized V1 model fallback with health-checking (NVIDIA NIM / OpenAI-compat).
The configured model can be dead (e.g. z-ai/glm-5.1 → 410 EOL). This module
health-checks a chain of models with a small STRUCTURED task and returns the
first model that actually works end-to-end (HTTP 200, valid JSON, schema-valid,
JD-traceable, no hallucinated phrase, within timeout). If none pass, callers get
`None` and must preserve the résumé and report `live_model_unavailable`.
Never combines partial outputs from different models: a failover re-runs the
whole operation on the replacement model.
"""
from __future__ import annotations
import time
from typing import List, Optional, Tuple
# Ordered preference. Kept centralized so every V1 route shares the same chain.
try:
from config import V1_MODEL_CHAIN as _CFG_CHAIN
except Exception:
_CFG_CHAIN = None
DEFAULT_MODEL_CHAIN: List[str] = _CFG_CHAIN or [
"z-ai/glm-5.2",
"mistralai/mistral-small-4-119b-2603",
"nvidia/nemotron-3-super-120b-a12b",
]
# A tiny probe JD with a known exact phrase and a decoy that must NOT be invented.
_PROBE_JD = (
"About the Role\nWe are hiring a Product Manager.\n"
"Requirements: strong stakeholder management and SQL. 5+ years experience.\n"
"Responsibilities: roadmap prioritization and A/B testing."
)
_PROBE_MUST_TRACE = "stakeholder management"
def health_check(model: str, timeout: float = 25.0) -> dict:
"""Probe one model with the real structured-extraction task and validate the
output the same way production does. Returns a log dict (never raises)."""
from .llm_client import LLMClient
from .keyword_schema import validate_and_repair
log = {
"requested_model": model, "selected_model": None, "status": "unknown",
"latency": 0.0, "retry_count": 0, "failure_reason": "",
"schema_valid": False, "evidence_test_passed": False,
}
t0 = time.monotonic()
try:
# Rebuild the OpenAI client with a SHORT per-request timeout so a hung
# model fails fast instead of blocking on the default 90s × retries.
from openai import OpenAI
from config import NVIDIA_API_KEY, GLM_BASE_URL
client = LLMClient(model=model)
client.client = OpenAI(base_url=GLM_BASE_URL, api_key=NVIDIA_API_KEY,
timeout=timeout, max_retries=0)
sys_p = ("Extract ATS keywords. Return ONLY a JSON array of objects with "
"keys exact_phrase, normalized_concept, category, requirement_type, "
"importance, source_text, semantic_variants, confidence, "
"requires_resume_evidence. No prose.")
rawtext = client._call(sys_p, f"JD:\n{_PROBE_JD}", max_tokens=1800, retries=2)
raw = client._extract_json(rawtext)
log["latency"] = round(time.monotonic() - t0, 2)
if not isinstance(raw, list) or not raw:
log["status"] = "empty_or_invalid"
log["failure_reason"] = "no structured items returned"
return log
valid, rejected = validate_and_repair(raw, _PROBE_JD)
log["schema_valid"] = bool(valid)
# traceability: the known phrase must be extracted; nothing untraceable kept
traced = any(_PROBE_MUST_TRACE in (v.get("exact_phrase", "").lower()
+ v.get("normalized_concept", "").lower())
for v in valid)
log["evidence_test_passed"] = bool(traced and valid)
if valid and traced:
log["status"] = "healthy"
log["selected_model"] = model
else:
log["status"] = "evidence_failure"
log["failure_reason"] = "probe phrase not traceably extracted"
except Exception as e:
log["latency"] = round(time.monotonic() - t0, 2)
msg = str(e)
log["failure_reason"] = msg[:160]
# classify a few common transient/terminal signals
if "410" in msg:
log["status"] = "gone_410"
elif "404" in msg:
log["status"] = "not_found_404"
elif "429" in msg:
log["status"] = "rate_limited_429"
elif "timeout" in msg.lower():
log["status"] = "timeout"
else:
log["status"] = "error"
return log
# Module cache: once a model is confirmed healthy, reuse it across requests rather
# than re-probing (which would pay the hung-model timeout every call). If that
# model later fails during real use, callers fall back to deterministic (safe);
# call reset_cache() to force a fresh probe.
_CACHE: dict = {"model": None, "logs": []}
def reset_cache() -> None:
_CACHE["model"] = None
_CACHE["logs"] = []
def extraction_gaps(clean_jd: str, valid_items: list) -> list:
"""Deterministic completeness check: obvious source-grounded requirements the
extraction should have captured but didn't. Returns a list of missed phrases
(named tools present in the JD, an explicit years-of-experience requirement).
Used to trigger a correction retry / failover before trusting an extraction."""
import re
try:
from .jd_analyzer import _TOOLS
except Exception:
_TOOLS = set()
low = (clean_jd or "").lower()
got = " ".join((v.get("exact_phrase", "") + " " + v.get("normalized_concept", ""))
for v in (valid_items or [])).lower()
missed = []
# named tools/technologies present verbatim in the JD but absent from criteria
for tool in _TOOLS:
if re.search(r"(?<![a-z0-9])" + re.escape(tool) + r"(?![a-z0-9])", low) \
and tool not in got:
missed.append(tool)
# explicit years-of-experience requirement (e.g. "10+ years")
ym = re.search(r"\b(\d{1,2}\+?\s*years?)\b", low)
if ym and "year" not in got:
missed.append(ym.group(1))
return missed[:12]
def select_model(chain: Optional[List[str]] = None,
timeout: float = 25.0, use_cache: bool = True) -> Tuple[Optional[str], List[dict]]:
"""Return (first_healthy_model | None, per-model health logs)."""
if use_cache and _CACHE["model"]:
return _CACHE["model"], _CACHE["logs"]
chain = chain or DEFAULT_MODEL_CHAIN
logs: List[dict] = []
for model in chain:
log = health_check(model, timeout=timeout)
logs.append(log)
if log["status"] == "healthy":
if use_cache:
_CACHE["model"], _CACHE["logs"] = model, logs
return model, logs
return None, logs
def build_llm(chain: Optional[List[str]] = None, timeout: float = 25.0,
use_cache: bool = True):
"""Return (LLMClient bound to a healthy model | None, health logs). None means
every model failed → caller must preserve résumé + report live_model_unavailable."""
from .llm_client import LLMClient
model, logs = select_model(chain, timeout=timeout, use_cache=use_cache)
if model is None:
return None, logs
return LLMClient(model=model), logs
if __name__ == "__main__": # live health-check (honest: reports real availability)
import json
model, logs = select_model()
print("SELECTED:", model)
for lg in logs:
print(json.dumps({k: lg[k] for k in
("requested_model", "status", "latency", "schema_valid",
"evidence_test_passed", "failure_reason")}, ensure_ascii=False))
|