File size: 8,099 Bytes
e0a3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
175
176
177
178
179
180
181
182
183
"""
Grounded reasoning generation.

Every reasoning string is assembled from facts that actually exist in the
candidate's profile and from the scorer's own per-component breakdown - never
from an LLM at ranking time. This guarantees the Stage-4 review checks pass:
specific facts, JD connection, honest concerns, no hallucination, variation,
and tone that matches the rank. Borderline candidates also get a counterfactual.

The output reads as plain English a recruiter could act on, not a template with
a name slotted in.
"""
from __future__ import annotations

from typing import Dict, List

from . import config, features

_GROUP_LABEL = {
    "retrieval_ranking": "retrieval/ranking",
    "embeddings": "embeddings",
    "vector_db": "vector search / hybrid search",
    "nlp": "NLP/LLM",
    "evaluation": "ranking evaluation (NDCG/MRR/A-B)",
    "ml_core": "applied ML",
}


def _evidence_phrase(scored: Dict) -> str:
    ev = scored["evidence"]
    present = [g for g in config.EVIDENCE_TERMS if ev.get(g, 0) > 0]
    # prioritise the IR-core groups for the phrase
    ordered = [g for g in ["retrieval_ranking", "embeddings", "vector_db",
                           "evaluation", "nlp", "ml_core"] if g in present]
    labels = [_GROUP_LABEL[g] for g in ordered[:3]]
    if not labels:
        return ""
    if len(labels) == 1:
        return labels[0]
    return ", ".join(labels[:-1]) + " and " + labels[-1]


def _top_skill_names(rec: dict, k: int = 3) -> List[str]:
    ai = [s for s in rec["skills"] if features._any_skill_is_ai(s)]
    ai.sort(key=lambda s: (s["assessment"] or 0, s["months"]), reverse=True)
    return [s["name"] for s in ai[:k]]


def confidence_tag(scored: Dict, trap: Dict) -> str:
    c = scored["components"]
    if trap["is_honeypot"]:
        return "Excluded"
    # "High" demands excellence across the board, not just a relevant title - so even
    # within the submitted top-100 (all strong by construction) the tag still varies:
    # a top pick that is off the 6-8y band or thinner on evidence reads "Moderate".
    strong = (c["title_role_fit"] >= 0.8 and c["domain_evidence"] >= 0.6
              and c["must_have_coverage"] >= 0.7 and c["experience_band"] >= 0.6)
    moderate = c["title_role_fit"] >= 0.55 or c["domain_evidence"] >= 0.3
    return "High" if strong else ("Moderate" if moderate else "Low")


def _concern(rec: dict, scored: Dict, trap: Dict) -> str:
    """One honest concern, if any - Stage-4 rewards acknowledging gaps."""
    c = scored["components"]
    sig = rec["signals"]
    if trap["is_stuffer"]:
        return "skills list is AI-heavy but the work history shows no ML/IR role"
    if c["must_have_coverage"] < 0.5:
        return "limited direct evidence on some JD must-haves (vector DB / ranking eval)"
    d = rec["days_since_active"]
    if d is not None and d > 150:
        return f"low availability ({d} days since last active)"
    rr = sig.get("recruiter_response_rate")
    if rr is not None and rr < 0.2:
        return f"weak recruiter response rate ({rr:.0%})"
    if features.consulting_only(rec):
        return "entire career at IT-services firms, no product-company experience"
    if features.location_class(rec) == "far" and not sig.get("willing_to_relocate"):
        return f"based in {rec['location']} and not open to relocation"
    if rec["yoe"] < config.EXP_OK_LO:
        return f"only {rec['yoe']:.0f} years experience, below the target band"
    return ""


def build_reasoning(rec: dict, scored: Dict, trap: Dict, rank: int,
                    counterfactual: bool = False) -> str:
    """Assemble a 1-2 sentence grounded reasoning for one candidate."""
    title = rec["title"]
    yoe = rec["yoe"]
    conf = confidence_tag(scored, trap)
    ev_phrase = _evidence_phrase(scored)
    skills = _top_skill_names(rec, 3)

    # Lead clause: who they are + the decisive evidence.
    lead = f"{title}, {yoe:.1f} yrs"
    if ev_phrase:
        lead += f"; demonstrated {ev_phrase}"
    elif skills:
        lead += f"; lists {', '.join(skills)}"

    # Availability / location colour where it helps.
    sig = rec["signals"]
    extras = []
    if features.location_class(rec) in ("preferred", "welcome"):
        extras.append(f"{rec['location']}-based")
    elif sig.get("willing_to_relocate"):
        extras.append("open to relocation")
    rr = sig.get("recruiter_response_rate")
    if rr is not None and rr >= 0.6 and conf in ("High", "Moderate"):
        extras.append(f"responsive to recruiters ({rr:.0%})")
    lead_extra = ("; " + ", ".join(extras)) if extras else ""

    concern = _concern(rec, scored, trap)
    if conf == "High":
        sent = f"{conf} confidence - {lead}{lead_extra}."
        if concern:
            sent += f" Minor concern: {concern}."
    elif conf == "Low":
        sent = f"{conf} confidence - {lead}{lead_extra}."
        sent += f" {concern[0].upper()}{concern[1:]}." if concern else " Adjacent fit only."
    else:
        sent = f"{conf} confidence - {lead}{lead_extra}."
        if concern:
            sent += f" Concern: {concern}."

    # Tone must scale with rank (Stage-4 check). Beyond the clear top tier, add an
    # honest "ranked here rather than higher because ..." note derived from this
    # candidate's own weakest dimension - keeps confident candidates from all
    # reading identically and acknowledges relative standing without faking doubt.
    note = ""
    if rank > 20:
        note = _relative_standing(rec, scored)
    elif counterfactual:
        note = _counterfactual(rec, scored)
    if note:
        sent += f" {note}"
    return sent.strip()


def _relative_standing(rec: dict, scored: Dict) -> str:
    """Surface the candidate's single WEAKEST dimension - the largest shortfall from
    an ideal hire - rather than the first matching rule. Picking the max gap (not a
    fixed priority order) keeps the clause honest *and* genuinely varied: different
    candidates have different weakest dimensions, so no single clause monopolizes the
    population the way a first-match chain does."""
    c = scored["components"]
    mods = scored["modifiers"]
    yoe = rec["yoe"]
    # Shortfalls normalized to a comparable 0..1 scale so the max is meaningful.
    gaps = {
        "experience": max(0.0, 0.9 - c["experience_band"]),
        "domain": max(0.0, 0.8 - c["domain_evidence"]),
        "availability": max(0.0, 0.95 - mods.get("availability", 1.0)) / 0.4,
        "skill_trust": max(0.0, 0.6 - c["skill_trust"]),
        "must_have": max(0.0, 0.85 - c["must_have_coverage"]),
    }
    worst = max(gaps, key=gaps.get)
    if gaps[worst] < 0.05:
        return "Ranked here rather than higher: edged out by candidates above with deeper retrieval/ranking track records."
    if worst == "experience":
        side = "below" if yoe < 6 else "above"
        return f"Ranked here rather than higher: {yoe:.1f}y sits {side} the 6-8y sweet spot."
    if worst == "domain":
        return "Ranked here rather than higher: retrieval/ranking evidence is solid but thinner than the top tier."
    if worst == "availability":
        return "Ranked here rather than higher: availability signals (recruiter response / recency) are a notch lower."
    if worst == "skill_trust":
        return "Ranked here rather than higher: skills are less corroborated by platform assessments."
    return "Ranked here rather than higher: doesn't yet evidence every JD must-have (e.g. ranking-evaluation depth)."


def _counterfactual(rec: dict, scored: Dict) -> str:
    """A cheap, specific 'what would move this candidate' note for borderline rows."""
    c = scored["components"]
    if c["domain_evidence"] < 0.5 and c["title_role_fit"] >= 0.6:
        return "Would rank materially higher with explicit vector-DB / ranking-eval evidence."
    if c["must_have_coverage"] < 0.75:
        return "Closing the gap on ranking-evaluation experience would lift this candidate."
    d = rec["days_since_active"]
    if d is not None and d > 120:
        return "Stronger if recent platform activity confirmed current availability."
    return ""