"""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 ". _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, }