Download src/rank/explain/compose.py from hembad/reranker: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/hembad/reranker/resolve/main/src/rank/explain/compose.py
- Command line
-
hf download hf://spaces/hembad/reranker/src/rank/explain/compose.py
-
curl -L -o compose.py https://huggingface.co/spaces/hembad/reranker/resolve/main/src/rank/explain/compose.py
17.3 kB
| """Layer 2 — advanced rule-based NLG: aggregate grounded facts into 3–4 fluent sentences (NO model). | |
| Builds ordered positive + negative (gap) clause pools from the banks, aggregates same-type facts under | |
| connectives, and assembles tier-shaped sentences. Phrasing variety comes from a ``VariantPicker`` — either blind | |
| ``random`` or grounded ``potion`` top-k (cosine of each variant vs the candidate's own evidence, then random | |
| among the top-k → grounded *and* varied). Negatives reach the CSV only where the tier allows (≤1 soft gap, T3/T4); | |
| the ``explanation`` text carries them all. Pure Python, microseconds per candidate. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| import re | |
| from typing import Any, Callable | |
| _LEAD_CONNECTIVE = re.compile(r"^(though|however,?|but|with|while)\s+", re.IGNORECASE) | |
| # Seniority words that may already sit in a title — if any is present, don't prefix the band label again. | |
| _TITLE_SENIORITY = ("senior", "staff", "lead", "principal", "junior", "head", "chief", "vp", "director", "intern") | |
| from jd import JDSpec | |
| from . import banks | |
| from .banks import BANKS, NEG_BANKS | |
| from .signals import BEHAVIOR_LEAD, BEHAVIOR_TAGS, PRIORITY, SIG_NEG_BANKS | |
| # A picker: (variant tuple, per-candidate evidence vector) → chosen variant string. | |
| Picker = Callable[[tuple[str, ...], Any], str] | |
| def random_picker(variants: tuple[str, ...], _evidence: Any = None) -> str: | |
| return random.choice(variants) | |
| class PotionPicker: | |
| """Grounded variant selection — cosine of each variant vs the candidate evidence, random among the top-k. | |
| Reuses the already-loaded potion-base-32M (Model2Vec). The fixed variant vocabulary is embedded once. Any | |
| failure (model missing, encode error) raises at construction so the caller falls back to ``random_picker``. | |
| """ | |
| def __init__(self, repo: str, topk: int) -> None: | |
| import numpy as np | |
| from retrieve.potion import _embed_unit, _load_potion | |
| self._np = np | |
| self._embed = _embed_unit | |
| self._model = _load_potion(repo) | |
| self._topk = max(1, topk) | |
| vecs = _embed_unit(self._model, list(banks.ALL_VARIANTS)) | |
| self._vec = {v: vecs[i] for i, v in enumerate(banks.ALL_VARIANTS)} | |
| def evidence_vector(self, text: str) -> Any: | |
| return self._embed(self._model, [text or "candidate"])[0] | |
| def _vec_of(self, variant: str) -> Any: | |
| """Variant embedding, lazily computed + cached for any slot not in the precomputed vocabulary.""" | |
| vec = self._vec.get(variant) | |
| if vec is None: | |
| vec = self._embed(self._model, [variant])[0] | |
| self._vec[variant] = vec | |
| return vec | |
| def __call__(self, variants: tuple[str, ...], evidence: Any) -> str: | |
| if evidence is None: | |
| return random.choice(variants) | |
| scored = sorted(variants, key=lambda v: float(evidence @ self._vec_of(v)), reverse=True) | |
| return random.choice(scored[: self._topk]) | |
| def make_picker(settings: Any) -> tuple[Picker, Any]: | |
| """Build the configured picker; return (picker, potion_or_None). Falls back to random on any failure.""" | |
| reasoning = settings.reasoning | |
| if reasoning.variant_select == "potion_topk": | |
| try: | |
| potion = PotionPicker(settings.models.embedder_broad, reasoning.variant_topk) | |
| return potion, potion | |
| except Exception as exc: # noqa: BLE001 — potion unavailable → blind random variety (still grounded by facts) | |
| from common.logging import get_logger | |
| get_logger("rank.explain").warning( | |
| "explain.potion_picker_unavailable", model=settings.models.embedder_broad, error=str(exc) | |
| ) | |
| return random_picker, None | |
| return random_picker, None | |
| def _fill(slot: str, picker: Picker, evidence: Any, neg: bool = False, **kw: Any) -> str: | |
| bank = (NEG_BANKS if neg else BANKS)[slot] | |
| return picker(bank, evidence).format(**kw) | |
| _FINAL_CONNECTIVE = ("and", "and", "and", "as well as") # mostly "and", occasional "as well as"; no casual "plus" | |
| _LEAD_JOINER = re.compile(r"^(and|plus|also|as well as)\s+", re.IGNORECASE) # defensive: no "and also brings…" | |
| def _oxford(items: list[str]) -> str: | |
| """Join clause fragments naturally with a varied final connective: 'a', 'a and b', 'a, b and c'.""" | |
| items = [_LEAD_JOINER.sub("", i.strip()) for i in items if i and i.strip()] | |
| if len(items) <= 1: | |
| return items[0] if items else "" | |
| conn = random.choice(_FINAL_CONNECTIVE) | |
| if len(items) == 2: | |
| return f"{items[0]} {conn} {items[1]}" | |
| return ", ".join(items[:-1]) + f" {conn} {items[-1]}" | |
| _A_BEFORE_VOWEL = re.compile(r"\b([Aa])\s+(?=[aeioAEIO])") # "a AI" → "an AI"; skip 'u' (keeps "a UI"/"a URL") | |
| def _fix_articles(text: str) -> str: | |
| """Correct the indefinite article before a vowel-letter word ('a AI Engineer' → 'an AI Engineer').""" | |
| return _A_BEFORE_VOWEL.sub(lambda m: ("An" if m.group(1) == "A" else "an") + " ", text) | |
| def _sentence(text: str) -> str: | |
| text = text.strip().rstrip(" ,;") | |
| if not text: | |
| return "" | |
| return _fix_articles(text[0].upper() + text[1:]) + "." | |
| def _evidence_text(facts: dict[str, Any]) -> str: | |
| """The candidate's own grounded evidence — what potion compares phrasing variants against.""" | |
| parts = [facts["title"], facts["seniority"], *facts["matched_skills"][:4], | |
| *facts["matched_industries"][:1], facts["city"]] | |
| return " ".join(p for p in parts if p) | |
| def _verdict_clauses(facts: dict[str, Any], picker: Picker, ev: Any) -> list[str]: | |
| """Sentence-1 VERDICT: how the candidate fits the role (experience band + title fit).""" | |
| out: list[str] = [] | |
| out.append(_fill(f"experience.{facts['exp_fit']}", picker, ev)) | |
| if facts["title_band"] == "align": | |
| out.append(_fill("title.align", picker, ev, title_jd=facts["title_match"])) | |
| elif facts["title_band"] == "adjacent": | |
| out.append(_fill("title.adjacent", picker, ev, title_jd=facts["title_match"])) | |
| return out | |
| def _strength_clauses(facts: dict[str, Any], picker: Picker, ev: Any, skills_cap: int) -> list[str]: | |
| """Sentence-2 STRENGTHS: concrete evidence (skills, industry, education, certs, location, adjacent depth).""" | |
| out: list[str] = [] | |
| if facts["matched_skills"]: | |
| slot = "skills.strong" if facts["skill_band"] == "strong" else "skills.partial" | |
| out.append(_fill(slot, picker, ev, skills=_oxford(facts["matched_skills"][:skills_cap]))) | |
| # Location high (recruiters care) so it survives the top-3 cap in S2. | |
| if facts["location_tier"] == "preferred": | |
| out.append(_fill("location.preferred", picker, ev, city=facts["city"])) | |
| elif facts["location_tier"] == "acceptable": | |
| out.append(_fill("location.acceptable", picker, ev, city=facts["city"])) | |
| if facts["matched_nice"] and not facts["matched_skills"]: # adjacent depth matters most when core is thin | |
| out.append(_fill("skills.nice", picker, ev, skills=_oxford(facts["matched_nice"][:skills_cap]))) | |
| if facts["matched_industries"]: | |
| out.append(_fill("industry.match", picker, ev, ind=facts["matched_industries"][0])) | |
| if facts["matched_education"]: | |
| out.append(_fill("education.match", picker, ev, field=facts["matched_education"][0])) | |
| if facts["matched_certs"]: | |
| out.append(_fill("cert.match", picker, ev, cert=facts["matched_certs"][0])) | |
| if facts["matched_nice"] and facts["matched_skills"]: # core present → nice-to-have is a bonus, lower priority | |
| out.append(_fill("skills.nice", picker, ev, skills=_oxford(facts["matched_nice"][:skills_cap]))) | |
| return out | |
| def _behavior_tags(facts: dict[str, Any], picker: Picker, ev: Any) -> list[str]: | |
| """Best-first, impactful behavior tags (deduped); recency leads, then the PRIORITY-ordered signals.""" | |
| sig = facts["signals"] | |
| tags: list[str] = [] | |
| if facts["recency_band"] in ("month", "quarter"): | |
| tags.append(picker(BEHAVIOR_TAGS["recency"], ev)) | |
| for key, want, slot, polarity in PRIORITY: | |
| if polarity != "pos" or slot not in BEHAVIOR_TAGS: | |
| continue | |
| value = sig.get(key) | |
| if value if want is None else value == want: | |
| tags.append(picker(BEHAVIOR_TAGS[slot], ev)) | |
| if facts["notice_band"] == "short": | |
| tags.append(picker(BEHAVIOR_TAGS["notice.short"], ev)) | |
| seen: set[str] = set() | |
| return [t for t in tags if not (t in seen or seen.add(t))] | |
| def _behavior_sentence(facts: dict[str, Any], picker: Picker, ev: Any) -> str: | |
| """Sentence-3 BEHAVIOR as a narrative — one of several structural forms (variety, never a fixed template).""" | |
| tags = _behavior_tags(facts, picker, ev)[:3] | |
| if not tags: | |
| return "" | |
| body = _oxford(tags) | |
| if not facts["signals"].get("active_market"): | |
| return body # lighter: just the behaviors, capitalized by _sentence | |
| forms = ( | |
| f"{picker(BEHAVIOR_LEAD, ev)}: {body}", | |
| f"behaviourally, this candidate is {body}", | |
| f"on engagement, they are {body}", | |
| f"the behavioural read is positive: {body}", | |
| body, | |
| ) | |
| return random.choice(forms) | |
| def _behavioral_negs(facts: dict[str, Any], picker: Picker, ev: Any) -> list[str]: | |
| """Behavioral GAP clauses (low responsiveness, thin profile, …) for the audit + the one allowed CSV gap.""" | |
| sig = facts["signals"] | |
| neg: list[str] = [] | |
| for key, want, slot, polarity in PRIORITY: | |
| if polarity != "neg": | |
| continue | |
| value = sig.get(key) | |
| if value if want is None else value == want: | |
| neg.append(picker(SIG_NEG_BANKS[slot], ev)) | |
| return neg | |
| def _gap_clauses(facts: dict[str, Any], picker: Picker, ev: Any) -> list[str]: | |
| """Ordered JD-fit gaps, most material first (missing must-have > title > location > company > notice/recency).""" | |
| gaps: list[str] = [] | |
| if facts["skill_band"] == "thin" and not facts["matched_skills"]: | |
| gaps.append(_fill("skills.thin", picker, ev, neg=True)) | |
| elif facts["missing_must"] and facts["skill_band"] != "strong": | |
| gaps.append(_fill("skills.missing", picker, ev, neg=True, | |
| missing=_oxford(facts["missing_must"][:2]))) | |
| if facts["title_band"] == "mismatch": | |
| gaps.append(_fill("title.mismatch", picker, ev, neg=True, title_jd=facts["title_match"])) | |
| if facts["location_tier"] == "outside": | |
| gaps.append(_fill("location.outside", picker, ev, neg=True)) | |
| if facts["country_fit"] == "outside": | |
| gaps.append(_fill("country.outside", picker, ev, neg=True)) | |
| if facts["exclude_hit"]: | |
| gaps.append(_fill("company.exclude", picker, ev, neg=True)) | |
| if facts["recency_band"] == "stale": | |
| gaps.append(_fill("recency.stale", picker, ev, neg=True)) | |
| if facts["notice_band"] == "long": | |
| gaps.append(_fill("notice.long", picker, ev, neg=True)) | |
| return gaps | |
| # Varied "but" pivots for the single soft gap (no dash). All must read with a NOUN-PHRASE gap (e.g. "thin overlap | |
| # on the core stack") — so subordinators / participials only, never "…is that <noun phrase>". | |
| _GAP_PIVOT = ("though", "albeit", "though notably", "the main gap being", "with the caveat of") | |
| def _gap_join(core: str, gap: str) -> str: | |
| """Fold ONE soft gap into a strengths clause with a varied pivot (composer owns the pivot).""" | |
| gap = _LEAD_CONNECTIVE.sub("", gap.strip()) | |
| pivot = random.choice(_GAP_PIVOT) | |
| if not core: | |
| return f"{pivot[0].upper() + pivot[1:]} {gap}" | |
| return f"{core}, {pivot} {gap}" | |
| def _assemble(lead: str, verdict: list[str], strengths: list[str], behavior: str, gap: str | None, | |
| signal: str, target: int) -> tuple[str, list[str]]: | |
| """A+B structure: S1 identity+verdict, S2 strengths(+gap), S3 behavior narrative, S4 why-ranked.""" | |
| s1 = _sentence(f"{lead}, {_oxford(verdict[:2])}" if verdict else lead) | |
| s2_core = _oxford(strengths[:3]) | |
| if gap: | |
| s2_core = _gap_join(s2_core, gap) | |
| s2 = _sentence(s2_core) | |
| s3 = _sentence(behavior) if behavior else "" | |
| s4 = _sentence(signal) | |
| sentences = [s for s in (s1, s2, s3, s4) if s] | |
| # Pad toward the target by promoting any leftover strength clauses into their own sentence (before the signal). | |
| leftover = strengths[3:] | |
| while len(sentences) < target and leftover: | |
| sentences.insert(-1, _sentence(_oxford(leftover[:2]))) | |
| leftover = leftover[2:] | |
| sentences = sentences[:target] | |
| return " ".join(sentences), sentences | |
| # The closing "why ranked" sentence — varied opener + the dominant signal + which JD fields matched lexically. | |
| # We deliberately DON'T restate the rank ("top 10") — that's already the rank column; instead we add the signal | |
| # and concrete BM25 field hits (more information, not redundant). | |
| _SIGNAL_LEAD = ("ranked on", "surfaced on", "stood out on", "selected on", "rose on the strength of", | |
| "shortlisted on") | |
| _BM25_FIELD_LABEL = {"skills": "skills", "titles": "title", "education": "education", | |
| "locations": "location", "industries": "industry"} | |
| # Natural ways to say "these fields are RELEVANT to the JD requirement" (not the mechanical "keyword matches on"). | |
| # Each entry INCLUDES its own leading punctuation/connector, and the openers AND the ending noun are varied | |
| # (role / brief / spec / posting / listing / needs / ask / requirements) so the closing clause never feels like a | |
| # fixed template. ``{fields}`` is filled with the relevant field list. | |
| _SIGNAL_MATCH = ( | |
| ", with {fields} relevant to the role", | |
| ", a strong match on {fields}", | |
| "; {fields} map cleanly to the brief", | |
| ", relevant across {fields}", | |
| "; {fields} fit what the role asks", | |
| "; their {fields} answer the spec", | |
| ", a close fit on {fields}", | |
| "; {fields} are on point for the posting", | |
| ", strong on the {fields} this role calls for", | |
| ", well-matched on {fields}", | |
| "; {fields} line up with the listing", | |
| "; {fields} meet the brief", | |
| ", relevant on {fields} for what the role needs", | |
| ", a clean match across {fields}", | |
| "; {fields} track the role's needs", | |
| "; {fields} speak to the requirements", | |
| ", squarely on the {fields} the role wants", | |
| "; {fields} answer the ask", | |
| ) | |
| def _signal_match_tail(hits: list[str], picker: Picker, ev: Any) -> str: | |
| """A varied, natural 'relevant to the requirement' tail (own punctuation); '' when nothing matched.""" | |
| if not hits: | |
| return "" | |
| return picker(_SIGNAL_MATCH, ev).format(fields=_oxford(hits[:3])) | |
| def _signal_sentence(facts: dict[str, Any], picker: Picker, ev: Any, brief: bool = False) -> str: | |
| """"Why ranked": dominant signal + which of the candidate's fields are RELEVANT to the JD (no rank ordinal).""" | |
| lead = picker(_SIGNAL_LEAD, ev) | |
| sentence = f"{lead} {facts['signal_phrase']}" | |
| hits = [_BM25_FIELD_LABEL[f] for f, hit in facts["bm25_hits"].items() if hit and f in _BM25_FIELD_LABEL] | |
| if not brief: # the relevance tail (own leading punctuation) is the first thing dropped when trimming length | |
| sentence += _signal_match_tail(hits, picker, ev) | |
| return sentence | |
| def compose_one( | |
| facts: dict[str, Any], spec: JDSpec, picker: Picker, ev_for: Callable[[dict], Any], | |
| *, sentences: int, allow_neg: bool, max_neg: int, skills_cap: int, | |
| ) -> dict[str, Any]: | |
| """Compose the NLG reasoning + explanation for one candidate at its tier settings.""" | |
| ev = ev_for(facts) | |
| # Avoid doubled seniority ("Senior Senior…", "Lead Staff…") — if the title already carries ANY seniority word | |
| # (even a different one than the band), lead with the title alone. | |
| seniority = facts["seniority"] | |
| title_l = facts["title"].lower() | |
| if seniority and (seniority.lower() in title_l or any(w in title_l for w in _TITLE_SENIORITY)): | |
| lead = facts["title"] | |
| else: | |
| lead = _fill("identity.lead", picker, ev, title=facts["title"], seniority=seniority) | |
| verdict = _verdict_clauses(facts, picker, ev) | |
| strengths = _strength_clauses(facts, picker, ev, skills_cap) | |
| behavior = _behavior_sentence(facts, picker, ev) | |
| gaps = _gap_clauses(facts, picker, ev) + _behavioral_negs(facts, picker, ev) | |
| signal = _signal_sentence(facts, picker, ev) | |
| csv_gap = gaps[0] if (allow_neg and max_neg > 0 and gaps) else None | |
| # No char-count trim: the sentence-count target (3-4) and skills_cap already bound the line structurally, | |
| # so a fixed char ceiling only ever discarded content (the keyword tail, then strengths) for no real reason. | |
| reasoning, sent_list = _assemble(lead, verdict, strengths, behavior, csv_gap, signal, sentences) | |
| # explanation = the NLG body + the full audit tail (ALL gaps, title detail) — always LLM-free. | |
| audit_bits = [] | |
| if gaps: | |
| audit_bits.append("Watch-outs: " + "; ".join(gaps) + ".") | |
| if facts["title_match"]: | |
| audit_bits.append(f"Title fit: {facts['title_match']} ({facts['title_band']}).") | |
| explanation = (reasoning + " " + " ".join(audit_bits)).strip() | |
| return { | |
| "reasoning": reasoning, | |
| "explanation": explanation, | |
| "sentences": sent_list, | |
| "positives": verdict + strengths + ([behavior] if behavior else []), | |
| "negatives": gaps, | |
| } | |