Files
srbhr--resume-matcher/apps/backend/e2e_monitor/judge.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

75 lines
3.7 KiB
Python

"""LLM-judge move — reuses the eval rubric via app.llm.complete_json."""
from __future__ import annotations
import json
from typing import Any
# Share of the tailored resume that may incorporate job-description keywords/skills
# the master resume lacked before the judge should treat it as fabrication rather
# than legitimate ATS tailoring. Maintainer policy (2026-06): surfacing JD keywords
# IS the product's job, so a moderate amount is expected and must not be scored as a
# truthfulness violation. This knob ONLY softens the judge's (LLM, qualitative)
# truthfulness lens — the hard structural guards in flow.score_tailoring
# (no_fabricated_employers, personal_info_unchanged) stay strict and are NOT affected.
# Trade-off (flagged at review): a higher value buys ATS match at the cost of letting
# more JD-sourced claims through; employers, titles, dates, and overall profession
# stay inviolate regardless. Dial this down to tighten truthfulness.
JD_KEYWORD_TOLERANCE = 0.20
_RUBRIC = ( # diverges from tests/evals/test_tailoring_eval.py by design — adds the JD tolerance
"You are a strict but fair technical recruiter grading how well a resume was "
"tailored to a job description on RELEVANCE, TRUTHFULNESS, and FORMATTING. "
"Incorporating job-description keywords and skills into the resume is EXPECTED, "
"legitimate tailoring (ATS optimization), not fabrication: do NOT lower the score "
f"when up to ~{int(JD_KEYWORD_TOLERANCE * 100)}% of the resume's content is "
"JD-sourced wording the master lacked, PROVIDED the candidate's employers, job "
"titles, dates, and overall profession remain unchanged. DO still penalize invented "
"employers, fabricated titles or dates, and a wholesale change of profession the "
"master never supported (e.g. a backend engineer rewritten as a career frontend dev). "
'Return ONLY JSON {"score": <int 1-5>, "reasons": "<one or two sentences>"}.'
)
def _normalize_score(raw: Any) -> int | None:
"""Coerce a judge score to an int in 1-5, or None. Rejects bools, non-finite, junk.
The whole conversion is wrapped in one try/except so a huge int (``float()``
OverflowError), ``inf``/``nan`` (``int()`` on a non-finite), or junk string
(``float()`` ValueError) all fail closed to ``None``. Uses round-half-up
(``int(x + 0.5)``) rather than ``round()``'s banker's rounding, since scores
are small positive integers.
"""
if isinstance(raw, bool):
return None
try:
if isinstance(raw, (int, float)):
candidate = float(raw)
elif isinstance(raw, str):
candidate = float(raw.strip())
else:
return None
value = int(candidate + 0.5) # round half up; raises on inf/nan
except (ValueError, OverflowError):
return None
return value if 1 <= value <= 5 else None
async def judge_variation(job_description: str, tailored: dict[str, Any]) -> dict[str, Any]:
"""Score one (JD, tailored) pair 1-5. Caller must be past the opt-in gate."""
from app.llm import complete_json
prompt = (
f"{_RUBRIC}\n\n=== JOB DESCRIPTION ===\n{job_description}\n\n"
f"=== TAILORED RESUME (JSON) ===\n{json.dumps(tailored, ensure_ascii=False, indent=2)}\n"
)
result = await complete_json(
prompt,
system_prompt="You are an impartial resume-tailoring evaluator.",
max_tokens=512,
schema_type="keywords", # "keywords" skips truncation heuristics; judge dict is accepted on the first call
)
if not isinstance(result, dict):
return {"score": None, "reasons": str(result)}
return {"score": _normalize_score(result.get("score")), "reasons": str(result.get("reasons", ""))}