JAA-ATS-Tool / src /external_ats.py
saitejatirunagari's picture
feat: LaTeX resume input + recruiter-grade keyword placement + resilient Run (Phase 7)
7759bfb
Raw
History Blame
8.35 kB
"""
External-ATS coverage simulator (Jobalytics/Simplify-style breadth).
WHY THIS EXISTS
───────────────
Our internal scorer extracts keywords from a NARROW PM taxonomy and scores the
resume against that same narrow set → it easily reports 90%+. External checkers
(Jobalytics/Simplify) extract a MUCH broader set (40-46 terms incl. domain words,
JD-specific responsibilities, soft skills) → the same resume scores ~54%. The
internal number is therefore NOT a valid success signal for the external goal.
This module computes a broad, Jobalytics-style EXPECTED keyword set from the JD,
and measures coverage against the *re-parsed exported resume text* (the only
truth). Used to (a) decide what to physically place in Maximum ATS Mode and
(b) gate/report readiness on real external-style coverage, not internal score.
It is intentionally broader than `extract_jd_keywords` (which stays skills-only
to keep the honest default pipeline clean). The broad set is only used in
Maximum ATS Mode / external-feedback repair, where the user has explicitly opted
into aggressive coverage.
"""
from __future__ import annotations
import re
from typing import Dict, List
from .ats_scorer import _kw_in_text, extract_jd_keywords
# Words that are never standalone keywords (checkers penalise/ignore them).
_STOP = {
"the", "and", "for", "with", "you", "your", "our", "are", "will", "have",
"has", "had", "this", "that", "these", "those", "from", "into", "out", "off",
"who", "what", "when", "where", "why", "how", "all", "any", "can", "may",
"should", "would", "could", "must", "able", "etc", "per", "via", "not",
"but", "they", "them", "their", "his", "her", "its", "she", "him", "was",
"were", "been", "being", "more", "most", "such", "than", "then", "also",
"about", "across", "within", "while", "each", "other", "some", "many",
"well", "very", "much", "like", "just", "only", "even", "both", "over",
"under", "between", "during", "including", "include", "includes",
"looking", "join", "join us", "work", "working", "team", "teams", "role",
"job", "company", "candidate", "candidates", "ideal", "great", "good",
"strong", "years", "year", "experience", "experiences", "plus", "preferred",
"required", "requirement", "responsibilities", "qualifications", "skills",
"ability", "knowledge", "understanding", "familiarity", "proficiency",
"we", "us", "is", "in", "on", "of", "to", "at", "as", "an", "or", "be",
"it", "by", "a", "i",
}
# Vague filler — never a keyword (mirrors jd_analyzer._BUZZWORDS intent).
_FILLER = {
"innovation", "innovative", "solution", "solutions", "tools", "tool",
"ownership", "synergy", "dynamic", "passionate", "motivated", "self-starter",
"results-driven", "detail-oriented", "team player", "track record",
"expertise", "best practices", "thought leadership", "fast-paced",
"cutting-edge", "world-class", "robust", "seamless", "holistic", "leverage",
"excellence", "proven", "successful", "goals", "productivity", "reinvent",
"reinvention", "mission", "vision", "culture", "value", "values", "impact",
"environment", "opportunity", "opportunities", "responsibility",
}
# UI/CTA/marketing phrases injected by checker overlays (Simplify/Jobalytics/…)
# — never genuine JD keywords. Exact lowercased matches.
_UI_NOISE = frozenset({
"show match details", "people clicked apply", "month free trial", "free trial",
"actively reviewing applicants", "message hiring managers",
"get personalized cover letter", "get insider access", "members use premium",
"recruiting bond", "visit website", "lacs pa", "help me stand",
"tailor my resume", "uses advanced ai", "try premium", "easy apply",
"see how you compare", "am i a good fit", "save job", "apply now",
"show more", "show less", "sign in", "join now",
})
# CTA/marketing lead tokens: a multi-word gram starting with one is UI noise.
_UI_LEAD = {
"show", "get", "try", "visit", "apply", "save", "click", "join", "sign",
"message", "see", "tailor", "unlock", "upgrade", "start",
}
# Marketing substrings that mark a gram as UI noise wherever they appear.
_UI_SUBSTR = (
"free trial", "premium", "hiring manager", "cover letter", "clicked apply",
"match details", "lacs pa",
)
def _clean(term: str) -> str:
return re.sub(r"[^\w\s/+.\-]", "", term or "").strip().lower()
def _is_ui_noise(t: str) -> bool:
"""True if `t` is a checker-overlay CTA / marketing phrase, not a JD term."""
t = (t or "").strip()
if not t:
return False
if t in _UI_NOISE:
return True
words = t.split()
if len(words) > 1 and words[0] in _UI_LEAD:
return True
if any(sub in t for sub in _UI_SUBSTR):
return True
return False
def _is_term_like(t: str) -> bool:
"""A token/phrase that reads like a real skill/responsibility/domain term."""
t = t.strip()
if not t or t in _STOP or t in _FILLER:
return False
if _is_ui_noise(t):
return False
words = t.split()
if len(words) > 4:
return False
if len(t) < 3:
return False
if t.isdigit():
return False
# All words must be non-stop and not lemmatizer artifacts.
for w in words:
if w in _STOP:
return False
if len(t) >= 5 and t.endswith(("at", "iz", "ic")) and len(words) == 1:
return False # "integrat", "automat", "operat"
return True
def extract_external_keywords(jd_text: str, extra: List[str] = None) -> List[str]:
"""Broad, Jobalytics-style expected keyword set for a JD.
Union of: the reliable taxonomy floor (`extract_jd_keywords`), curated
Maximum-ATS safe terms present in the JD, and term-like 1-3 word phrases
pulled from the JD body (filtered against stopwords/filler). De-duplicated,
lowercase. `extra` (e.g. pasted external missing/matched terms) is unioned in.
"""
jd_text = jd_text or ""
jd_low = jd_text.lower()
out: List[str] = []
seen = set()
def _add(term: str):
t = _clean(term)
if t and t not in seen and _is_term_like(t):
seen.add(t)
out.append(t)
# 1. Reliable taxonomy floor.
for k in extract_jd_keywords(jd_text):
_add(k)
# 2. Curated safe vocabulary that actually appears in this JD.
try:
from config import MAXIMUM_ATS_SAFE_TERMS as _SAFE
except Exception:
_SAFE = set()
for t in _SAFE:
if t in jd_low:
_add(t)
# 3. Term-like phrases from the JD body. Pull capitalised/section phrases and
# notable bigrams/trigrams. Conservative: only multiword phrases whose
# words are all alphabetic and non-stop, plus known-good single nouns.
# (Single-noun garbage is filtered by _is_term_like + the dedup below.)
# Bi/tri-grams of alphabetic words.
words = re.findall(r"[a-zA-Z][a-zA-Z\-/+.]{1,}", jd_low)
for n in (3, 2):
for i in range(len(words) - n + 1):
gram = " ".join(words[i:i + n])
if all(w not in _STOP and w not in _FILLER for w in words[i:i + n]):
# Only keep grams that recur or look like a skill phrase.
if jd_low.count(gram) >= 1 and len(gram) <= 34:
# Skip grams that are mostly filler-ish single words joined.
_add(gram)
# 4. Pasted external terms (ground truth from a checker).
for t in (extra or []):
_add(t)
return out
def external_coverage(expected: List[str], exported_text: str) -> Dict:
"""Measure coverage of `expected` terms against the exported resume text.
Returns {expected, found, pct, present, missing}. Matching uses the same
word-boundary/phrase logic as scoring (`_kw_in_text`).
"""
text = (exported_text or "").lower()
exp = []
seen = set()
for t in expected:
tl = _clean(t)
if tl and tl not in seen:
seen.add(tl)
exp.append(tl)
present = [t for t in exp if _kw_in_text(t, text)]
missing = [t for t in exp if t not in present]
total = len(exp)
return {
"expected": total,
"found": len(present),
"pct": int(round(100 * len(present) / max(1, total))),
"present": present,
"missing": missing,
}