Hemprasad Badgujar commited on
Commit
c408892
·
1 Parent(s): 190f0fc

Remove char limits; expand template variants

Browse files

Removed arbitrary character ceilings that silently discarded well-formed explanations without improving output quality. Instead:

- Relaxed reasoning char limit in compose.py (removed 500 char trim)
- Separated summary and experience truncation in engine.py (independent 500/900 budgets) so verbose summaries no longer starve role descriptions
- Removed upper bound in tiers.py polish() check (kept 120 char floor)
- Expanded all template banks from ~6 to 9 variants each for better phrasing diversity
- Expanded BEHAVIOR_TAGS from 3-4 to 5 variants per slot

Length constraints are now enforced at generation time (max_tokens) and validation (floor only), allowing grounded, well-structured prose through.

src/rank/explain/banks.py CHANGED
@@ -1,6 +1,6 @@
1
  """Phrasing-variant banks for the NLG composer + the tiny-model salvage/grounding gates.
2
 
3
- Each *slot* (e.g. ``location.preferred``) carries **≥6** template variants — the composer picks one per
4
  candidate (random, or grounded top-k via potion). Placeholders are filled ONLY from the FactEngine dict, never
5
  invented. Polarity is encoded in the slot name (``*.neg`` slots are gaps). Numbers never appear as raw floats —
6
  the bands map them to words first (see ``bands.py``).
@@ -16,71 +16,91 @@ BANKS: dict[str, tuple[str, ...]] = {
16
  "identity.lead": (
17
  "{title}", "{seniority} {title}", "{title} ({seniority} profile)",
18
  "a {seniority} {title}", "{title}, {seniority}-level", "{title} at a {seniority} level",
 
19
  ),
20
  "experience.in": (
21
  "experience on target for this role", "within the JD experience range", "seniority fits the stated band",
22
  "right in the target experience window", "tenure matches what the role asks", "squarely in the wanted band",
 
 
23
  ),
24
  "experience.below": (
25
  "a touch early for the stated range", "lighter experience than the JD target",
26
  "slightly under the wanted band", "earlier-career for this role", "a little below the target tenure",
27
- "newer than the posted experience band",
 
28
  ),
29
  "experience.above": (
30
  "more tenured than the JD upper range", "senior beyond the posted band", "above the stated experience window",
31
  "carries more years than asked", "over the target tenure", "more seasoned than the band requires",
 
 
32
  ),
33
  "skills.strong": (
34
  "strong on {skills}", "strong overlap on {skills}", "covers the core stack: {skills}",
35
  "deep on {skills}", "solid command of {skills}", "well-matched on {skills}",
 
36
  ),
37
  "skills.partial": (
38
  "partial fit, with {skills}", "brings some of the stack ({skills})", "covers {skills} among the must-haves",
39
  "a working overlap on {skills}", "matches part of the stack: {skills}", "hits {skills} from the requirements",
 
40
  ),
41
  "skills.nice": ( # no leading connective ("plus"/"also") — the composer adds the join word
42
  "bonus depth in {skills}", "adjacent strengths in {skills}", "added breadth in {skills}",
43
  "nice-to-have coverage on {skills}", "extra range across {skills}", "rounded out by {skills}",
 
44
  ),
45
  "title.align": (
46
  "title aligns with {title_jd}", "a direct {title_jd} match", "role maps cleanly to {title_jd}",
47
  "exactly the {title_jd} profile", "titled right for {title_jd}", "a {title_jd} by current role",
 
 
48
  ),
49
  "title.adjacent": (
50
  "an adjacent role to {title_jd}", "a near-fit for {title_jd}", "close to the {title_jd} track",
51
  "a neighbouring role to {title_jd}", "in the {title_jd} family", "a sibling role to {title_jd}",
 
52
  ),
53
  "location.preferred": (
54
  "based in {city}", "in preferred city {city}", "located in {city}",
55
  "{city}-based", "sits in {city}", "working out of {city}",
 
56
  ),
57
  "location.acceptable": (
58
  "in acceptable city {city}", "based in {city} (acceptable)", "located in {city}, within reach",
59
  "{city}-based, on the acceptable list", "in {city}, a workable location", "sits in {city}, still in scope",
 
60
  ),
61
  "industry.match": (
62
  "{ind} background", "a background in {ind}", "comes from {ind}",
63
  "{ind} domain experience", "rooted in {ind}", "industry fit in {ind}",
 
64
  ),
65
  "education.match": (
66
  "a {field} degree", "studied {field}", "academic grounding in {field}",
67
  "educated in {field}", "a {field} background academically", "degree in {field}",
 
68
  ),
69
  "cert.match": (
70
  "certified in {cert}", "holds a {cert} credential", "a {cert} certification",
71
  "credentialed on {cert}", "carries {cert} certification", "{cert}-certified",
 
72
  ),
73
  "recency.month": (
74
  "active this month", "on the platform this month", "freshly active",
75
  "active very recently", "engaged this month", "newly active on the platform",
 
76
  ),
77
  "recency.quarter": (
78
  "active this quarter", "recently active on the platform", "active in the last quarter",
79
  "engaged recently", "seen on the platform this quarter", "active of late",
 
80
  ),
81
  "notice.short": (
82
  "available on short notice", "can start quickly", "ready to join soon",
83
  "a short notice period", "quick to onboard", "available to begin shortly",
 
84
  ),
85
  }
86
 
@@ -90,36 +110,46 @@ NEG_BANKS: dict[str, tuple[str, ...]] = {
90
  "thin overlap on the must-have skills", "light on several core requirements",
91
  "only a loose skill match", "limited coverage of the core stack",
92
  "missing much of the required stack", "a sparse fit on the must-haves",
 
 
93
  ),
94
  "skills.missing": (
95
  "no explicit {missing}", "though {missing} isn't surfaced", "missing {missing} from the stack",
96
  "without an explicit {missing} signal", "{missing} not evidenced", "light on {missing}",
 
97
  ),
98
  "title.mismatch": (
99
  "though the title sits off the {title_jd} track", "with a title outside the {title_jd} line",
100
  "title doesn't map to {title_jd}", "a different title than {title_jd}",
101
  "off-title versus {title_jd}", "not titled as {title_jd}",
 
102
  ),
103
  "location.outside": (
104
  "outside the listed work cities", "not in a preferred location", "based outside the JD's city list",
105
  "away from the listed locations", "off the preferred-city list", "in a non-listed city",
 
106
  ),
107
  "country.outside": (
108
  "outside the accepted countries", "in a non-listed country", "beyond the country scope",
109
  "not in an accepted country", "outside the geographic scope", "in an out-of-scope country",
 
110
  ),
111
  "company.exclude": (
112
  "currently at a dispreferred employer", "at a company on the exclude list",
113
  "in a services-heavy current role", "at a less-preferred current employer",
114
  "currently with a dispreferred firm", "at an excluded current company",
 
 
115
  ),
116
  "recency.stale": (
117
  "quiet on the platform lately", "not active recently", "a stale activity record",
118
  "little recent platform activity", "dormant of late", "no recent platform sign-in",
 
119
  ),
120
  "notice.long": (
121
  "a longer notice period", "slower to become available", "a long notice runway",
122
  "not immediately available", "an extended notice period", "longer to onboard",
 
123
  ),
124
  }
125
 
 
1
  """Phrasing-variant banks for the NLG composer + the tiny-model salvage/grounding gates.
2
 
3
+ Each *slot* (e.g. ``location.preferred``) carries **9** template variants — the composer picks one per
4
  candidate (random, or grounded top-k via potion). Placeholders are filled ONLY from the FactEngine dict, never
5
  invented. Polarity is encoded in the slot name (``*.neg`` slots are gaps). Numbers never appear as raw floats —
6
  the bands map them to words first (see ``bands.py``).
 
16
  "identity.lead": (
17
  "{title}", "{seniority} {title}", "{title} ({seniority} profile)",
18
  "a {seniority} {title}", "{title}, {seniority}-level", "{title} at a {seniority} level",
19
+ "a {seniority}-level {title}", "{title}, operating at {seniority} level", "{seniority}-track {title}",
20
  ),
21
  "experience.in": (
22
  "experience on target for this role", "within the JD experience range", "seniority fits the stated band",
23
  "right in the target experience window", "tenure matches what the role asks", "squarely in the wanted band",
24
+ "years of experience land right where the role wants them", "tenure sits comfortably inside the target band",
25
+ "experience checks out against the stated range",
26
  ),
27
  "experience.below": (
28
  "a touch early for the stated range", "lighter experience than the JD target",
29
  "slightly under the wanted band", "earlier-career for this role", "a little below the target tenure",
30
+ "newer than the posted experience band", "a step behind the stated experience window",
31
+ "shy of the tenure the role targets", "earlier in their career than the band implies",
32
  ),
33
  "experience.above": (
34
  "more tenured than the JD upper range", "senior beyond the posted band", "above the stated experience window",
35
  "carries more years than asked", "over the target tenure", "more seasoned than the band requires",
36
+ "past the top of the stated experience range", "brings more tenure than the role calls for",
37
+ "well beyond the posted seniority band",
38
  ),
39
  "skills.strong": (
40
  "strong on {skills}", "strong overlap on {skills}", "covers the core stack: {skills}",
41
  "deep on {skills}", "solid command of {skills}", "well-matched on {skills}",
42
+ "a strong hand with {skills}", "clearly fluent in {skills}", "checks every box on {skills}",
43
  ),
44
  "skills.partial": (
45
  "partial fit, with {skills}", "brings some of the stack ({skills})", "covers {skills} among the must-haves",
46
  "a working overlap on {skills}", "matches part of the stack: {skills}", "hits {skills} from the requirements",
47
+ "some grounding in {skills}", "picks up {skills} from the core list", "a partial read on {skills}",
48
  ),
49
  "skills.nice": ( # no leading connective ("plus"/"also") — the composer adds the join word
50
  "bonus depth in {skills}", "adjacent strengths in {skills}", "added breadth in {skills}",
51
  "nice-to-have coverage on {skills}", "extra range across {skills}", "rounded out by {skills}",
52
+ "extra depth in {skills} beyond the core ask", "a useful add-on in {skills}", "broadens the profile with {skills}",
53
  ),
54
  "title.align": (
55
  "title aligns with {title_jd}", "a direct {title_jd} match", "role maps cleanly to {title_jd}",
56
  "exactly the {title_jd} profile", "titled right for {title_jd}", "a {title_jd} by current role",
57
+ "current title lines up with {title_jd}", "a clean {title_jd} fit by title",
58
+ "titled the same as the role: {title_jd}",
59
  ),
60
  "title.adjacent": (
61
  "an adjacent role to {title_jd}", "a near-fit for {title_jd}", "close to the {title_jd} track",
62
  "a neighbouring role to {title_jd}", "in the {title_jd} family", "a sibling role to {title_jd}",
63
+ "one step over from {title_jd}", "in the same lane as {title_jd}", "a related track to {title_jd}",
64
  ),
65
  "location.preferred": (
66
  "based in {city}", "in preferred city {city}", "located in {city}",
67
  "{city}-based", "sits in {city}", "working out of {city}",
68
+ "calling {city} home", "on the ground in {city}", "{city} based, on the preferred list",
69
  ),
70
  "location.acceptable": (
71
  "in acceptable city {city}", "based in {city} (acceptable)", "located in {city}, within reach",
72
  "{city}-based, on the acceptable list", "in {city}, a workable location", "sits in {city}, still in scope",
73
+ "{city}-based, within the accepted range", "located in {city}, a fine fallback city", "in {city}, which still counts",
74
  ),
75
  "industry.match": (
76
  "{ind} background", "a background in {ind}", "comes from {ind}",
77
  "{ind} domain experience", "rooted in {ind}", "industry fit in {ind}",
78
+ "time spent in {ind}", "{ind} experience on the resume", "a track record in {ind}",
79
  ),
80
  "education.match": (
81
  "a {field} degree", "studied {field}", "academic grounding in {field}",
82
  "educated in {field}", "a {field} background academically", "degree in {field}",
83
+ "trained academically in {field}", "{field} on the transcript", "formal study in {field}",
84
  ),
85
  "cert.match": (
86
  "certified in {cert}", "holds a {cert} credential", "a {cert} certification",
87
  "credentialed on {cert}", "carries {cert} certification", "{cert}-certified",
88
+ "{cert} certified and current", "carries the {cert} credential", "backed by a {cert} certification",
89
  ),
90
  "recency.month": (
91
  "active this month", "on the platform this month", "freshly active",
92
  "active very recently", "engaged this month", "newly active on the platform",
93
+ "signed in within the month", "active on the platform just this month", "fresh activity this month",
94
  ),
95
  "recency.quarter": (
96
  "active this quarter", "recently active on the platform", "active in the last quarter",
97
  "engaged recently", "seen on the platform this quarter", "active of late",
98
+ "active within the last few months", "on the platform sometime this quarter", "shown recent activity this quarter",
99
  ),
100
  "notice.short": (
101
  "available on short notice", "can start quickly", "ready to join soon",
102
  "a short notice period", "quick to onboard", "available to begin shortly",
103
+ "free to start on short notice", "able to join without a long wait", "a fast turnaround to start",
104
  ),
105
  }
106
 
 
110
  "thin overlap on the must-have skills", "light on several core requirements",
111
  "only a loose skill match", "limited coverage of the core stack",
112
  "missing much of the required stack", "a sparse fit on the must-haves",
113
+ "little overlap on the core requirements", "a weak match on the must-have list",
114
+ "covers only a fraction of the core stack",
115
  ),
116
  "skills.missing": (
117
  "no explicit {missing}", "though {missing} isn't surfaced", "missing {missing} from the stack",
118
  "without an explicit {missing} signal", "{missing} not evidenced", "light on {missing}",
119
+ "no clear signal on {missing}", "{missing} doesn't show up in the profile", "unproven on {missing}",
120
  ),
121
  "title.mismatch": (
122
  "though the title sits off the {title_jd} track", "with a title outside the {title_jd} line",
123
  "title doesn't map to {title_jd}", "a different title than {title_jd}",
124
  "off-title versus {title_jd}", "not titled as {title_jd}",
125
+ "a title that diverges from {title_jd}", "outside the {title_jd} title track", "carries a title unlike {title_jd}",
126
  ),
127
  "location.outside": (
128
  "outside the listed work cities", "not in a preferred location", "based outside the JD's city list",
129
  "away from the listed locations", "off the preferred-city list", "in a non-listed city",
130
+ "not based in one of the listed cities", "outside the role's target cities", "located beyond the preferred list",
131
  ),
132
  "country.outside": (
133
  "outside the accepted countries", "in a non-listed country", "beyond the country scope",
134
  "not in an accepted country", "outside the geographic scope", "in an out-of-scope country",
135
+ "based in a country outside scope", "not within the accepted geography", "outside the role's country list",
136
  ),
137
  "company.exclude": (
138
  "currently at a dispreferred employer", "at a company on the exclude list",
139
  "in a services-heavy current role", "at a less-preferred current employer",
140
  "currently with a dispreferred firm", "at an excluded current company",
141
+ "at a firm the role screens out", "currently with an out-of-scope employer",
142
+ "employed at a company outside preference",
143
  ),
144
  "recency.stale": (
145
  "quiet on the platform lately", "not active recently", "a stale activity record",
146
  "little recent platform activity", "dormant of late", "no recent platform sign-in",
147
+ "hasn't been active in a while", "quiet for a stretch on the platform", "activity has gone stale recently",
148
  ),
149
  "notice.long": (
150
  "a longer notice period", "slower to become available", "a long notice runway",
151
  "not immediately available", "an extended notice period", "longer to onboard",
152
+ "a stretched-out notice period", "will take longer than most to start", "a slower runway to availability",
153
  ),
154
  }
155
 
src/rank/explain/compose.py CHANGED
@@ -333,13 +333,9 @@ def compose_one(
333
  signal = _signal_sentence(facts, picker, ev)
334
 
335
  csv_gap = gaps[0] if (allow_neg and max_neg > 0 and gaps) else None
 
 
336
  reasoning, sent_list = _assemble(lead, verdict, strengths, behavior, csv_gap, signal, sentences)
337
- if len(reasoning) > 500: # keep the CSV line readable: drop the keyword tail, then trim strengths (2 → 1)
338
- brief_signal = _signal_sentence(facts, picker, ev, brief=True)
339
- for cap in (2, 1):
340
- reasoning, sent_list = _assemble(lead, verdict, strengths[:cap], behavior, csv_gap, brief_signal, sentences)
341
- if len(reasoning) <= 500:
342
- break
343
 
344
  # explanation = the NLG body + the full audit tail (ALL gaps, title detail) — always LLM-free.
345
  audit_bits = []
 
333
  signal = _signal_sentence(facts, picker, ev)
334
 
335
  csv_gap = gaps[0] if (allow_neg and max_neg > 0 and gaps) else None
336
+ # No char-count trim: the sentence-count target (3-4) and skills_cap already bound the line structurally,
337
+ # so a fixed char ceiling only ever discarded content (the keyword tail, then strengths) for no real reason.
338
  reasoning, sent_list = _assemble(lead, verdict, strengths, behavior, csv_gap, signal, sentences)
 
 
 
 
 
 
339
 
340
  # explanation = the NLG body + the full audit tail (ALL gaps, title detail) — always LLM-free.
341
  audit_bits = []
src/rank/explain/engine.py CHANGED
@@ -96,14 +96,15 @@ def _recently_active(row: dict[str, Any], recent_days: int) -> bool:
96
  return days is not None and float(days) <= recent_days
97
 
98
 
99
- def _evidence_snippet(row: dict[str, Any], limit: int = 900) -> str:
100
  """The candidate's OWN WORDS (profile summary + recent-role descriptions) — the raw material for an
101
- outcome-oriented recommendation. Truncated; the LLM extracts a concrete achievement from this, the NLG ignores
102
- it (it can only template grounded facts)."""
103
- summary = str(row.get("summary") or "").strip()
104
- experience = str(row.get("experience_text") or "").strip().replace("\n", "; ")
105
- text = f"{summary} {experience}".strip()
106
- return text[:limit]
 
107
 
108
 
109
  def build_facts(
 
96
  return days is not None and float(days) <= recent_days
97
 
98
 
99
+ def _evidence_snippet(row: dict[str, Any], summary_limit: int = 500, experience_limit: int = 900) -> str:
100
  """The candidate's OWN WORDS (profile summary + recent-role descriptions) — the raw material for an
101
+ outcome-oriented recommendation. The LLM extracts a concrete achievement from this, the NLG ignores it (it
102
+ can only template grounded facts). Summary and experience are truncated INDEPENDENTLY (not one combined
103
+ budget) — a verbose summary would otherwise eat the whole limit and starve out the role bullets, which is
104
+ where the concrete numbers (users served, % improvement, scale) actually live."""
105
+ summary = str(row.get("summary") or "").strip()[:summary_limit]
106
+ experience = str(row.get("experience_text") or "").strip().replace("\n", "; ")[:experience_limit]
107
+ return f"{summary} {experience}".strip()
108
 
109
 
110
  def build_facts(
src/rank/explain/signals.py CHANGED
@@ -10,56 +10,72 @@ from __future__ import annotations
10
 
11
  from typing import Any
12
 
13
- # Behavioral positive slots (≥6 variants for the heavy hitters; compact for the rare ones).
14
  SIG_BANKS: dict[str, tuple[str, ...]] = {
15
  "intent.open": (
16
  "open to work and exploring roles", "actively open to opportunities", "signalling they're in the market",
17
  "flagged open to work", "looking for their next move", "marked open to new roles",
 
18
  ),
19
  "availability.relocate": (
20
  "open to relocating", "willing to relocate", "flexible on location",
21
  "happy to move for the role", "relocation-friendly", "open to a move",
 
22
  ),
23
  "availability.hybrid": (
24
  "prefers hybrid work", "set up for hybrid working", "aligned to a hybrid mode",
25
  "comfortable with hybrid", "a hybrid-work preference", "suited to hybrid",
 
26
  ),
27
  "responsive.high": (
28
  "responds reliably to recruiters", "quick to reply on the platform", "highly responsive to outreach",
29
  "a reliable communicator with recruiters", "answers recruiter messages promptly", "strong recruiter responsiveness",
 
30
  ),
31
  "assessment.high": (
32
  "strong platform skill assessments", "high verified-skill scores", "tested well on platform assessments",
33
  "validated by skill assessments", "strong assessed competence", "high assessment marks",
 
 
34
  ),
35
  "reachability.full": (
36
  "easy to reach on verified channels", "fully verified contact details", "reachable via verified email, phone and LinkedIn",
37
  "verified across contact channels", "a fully verified profile", "easy to contact, all channels verified",
 
38
  ),
39
  "reachability.partial": (
40
  "reachable on verified channels", "verified email and phone on file", "contactable on verified details",
41
  "partly verified contact info", "verified on a couple of channels", "reachable, partially verified",
 
42
  ),
43
  "interest.high": (
44
  "drawing recruiter interest lately", "saved by recruiters recently", "getting strong recruiter views",
45
  "shortlisted by recruiters of late", "surfacing in recruiter searches", "noticed by recruiters this month",
 
46
  ),
47
  "hiring.reliable": (
48
  "reliably completes interviews", "a dependable interview track record", "shows up for scheduled interviews",
49
  "consistent through hiring processes", "follows through on interviews", "a reliable hiring participant",
 
 
50
  ),
51
  "github.active": (
52
  "recent public GitHub activity", "active in open source lately", "visible technical work on GitHub",
53
  "an active public code footprint", "shipping in public on GitHub", "recent open-source contributions",
 
54
  ),
55
  "integrity.complete": (
56
  "a complete, detailed profile", "high profile completeness", "a thorough, well-filled profile",
57
  "a fully fleshed-out profile", "comprehensive profile detail", "a richly completed profile",
 
 
58
  ),
59
  "soft_pattern.active_market": (
60
  "active, open to work and responsive to recruiters", "recently active, reachable and in the market",
61
  "engaged on the platform and open to roles", "visible to recruiters and quick to respond",
62
  "in the market with strong platform engagement", "active, contactable and open to opportunities",
 
 
63
  ),
64
  }
65
 
@@ -67,18 +83,25 @@ SIG_NEG_BANKS: dict[str, tuple[str, ...]] = {
67
  "responsive.low": (
68
  "slow to respond to recruiters", "low recruiter responsiveness", "quiet on recruiter outreach",
69
  "infrequent platform replies", "passive on recruiter messages", "a low response rate",
 
70
  ),
71
  "reachability.minimal": (
72
  "limited verified contact info", "few verified contact channels", "thinly verified contact details",
73
  "hard to reach on verified channels", "minimal contact verification", "sparse verified contacts",
 
 
74
  ),
75
  "hiring.patchy": (
76
  "a patchy interview-attendance history", "inconsistent through interviews", "spotty interview follow-through",
77
  "an uneven hiring-process record", "misses scheduled interviews at times", "a patchy interview record",
 
 
78
  ),
79
  "integrity.thin": (
80
  "a thin or incomplete profile", "low profile completeness", "a sparsely filled profile",
81
  "limited profile detail", "an under-filled profile", "a bare-bones profile",
 
 
82
  ),
83
  }
84
 
@@ -106,24 +129,35 @@ PRIORITY: tuple[tuple[str, Any, str, str], ...] = (
106
  )
107
 
108
  # Compact, IMPACTFUL behavior tags for the narrative sentence (distinct from the fuller SIG_BANKS clauses) —
109
- # framed as candidate BEHAVIOR. ≥3 variants each for run-to-run variety. Keyed by the PRIORITY slot.
110
  BEHAVIOR_TAGS: dict[str, tuple[str, ...]] = {
111
- "recency": ("recently active", "active on the platform", "recently engaged", "active of late"),
112
- "intent.open": ("open to work", "actively exploring roles", "in the market", "looking to move"),
 
 
113
  "interest.high": ("in active recruiter demand", "drawing recruiter interest", "attracting recruiter attention",
114
- "shortlisted by recruiters"),
115
  "responsive.high": ("responsive to recruiters", "quick to reply", "a responsive communicator",
116
- "fast to respond"),
117
  "github.active": ("active in open source", "shipping public code", "visible in open source",
118
- "contributing on GitHub"),
119
- "assessment.high": ("assessment-validated", "strong on skill assessments", "tested well on assessments"),
120
- "availability.relocate": ("open to relocating", "willing to relocate", "geographically flexible"),
121
- "availability.hybrid": ("set for hybrid", "hybrid-ready", "aligned to hybrid work"),
122
- "hiring.reliable": ("reliable through interviews", "dependable in hiring", "consistent in the process"),
123
- "reachability.full": ("easy to reach", "fully reachable", "readily contactable"),
124
- "reachability.partial": ("reachable on verified channels", "contactable on verified details"),
125
- "integrity.complete": ("a complete, detailed profile", "thoroughly filled out", "a well-developed profile"),
126
- "notice.short": ("available soon", "able to start quickly", "ready to move quickly"),
 
 
 
 
 
 
 
 
 
127
  }
128
 
129
  # Lead-ins for the behavior sentence when the candidate is broadly active in the market (≥2 strong flags).
 
10
 
11
  from typing import Any
12
 
13
+ # Behavioral positive slots (9 variants each).
14
  SIG_BANKS: dict[str, tuple[str, ...]] = {
15
  "intent.open": (
16
  "open to work and exploring roles", "actively open to opportunities", "signalling they're in the market",
17
  "flagged open to work", "looking for their next move", "marked open to new roles",
18
+ "openly job hunting right now", "actively weighing new roles", "flagged as open to new opportunities",
19
  ),
20
  "availability.relocate": (
21
  "open to relocating", "willing to relocate", "flexible on location",
22
  "happy to move for the role", "relocation-friendly", "open to a move",
23
+ "game for relocating if needed", "flexible enough to relocate", "would consider a move for the right role",
24
  ),
25
  "availability.hybrid": (
26
  "prefers hybrid work", "set up for hybrid working", "aligned to a hybrid mode",
27
  "comfortable with hybrid", "a hybrid-work preference", "suited to hybrid",
28
+ "leans toward a hybrid setup", "fine with splitting time in office and remote", "geared for hybrid work",
29
  ),
30
  "responsive.high": (
31
  "responds reliably to recruiters", "quick to reply on the platform", "highly responsive to outreach",
32
  "a reliable communicator with recruiters", "answers recruiter messages promptly", "strong recruiter responsiveness",
33
+ "replies fast when recruiters reach out", "consistently responsive to messages", "easy to get a quick reply from",
34
  ),
35
  "assessment.high": (
36
  "strong platform skill assessments", "high verified-skill scores", "tested well on platform assessments",
37
  "validated by skill assessments", "strong assessed competence", "high assessment marks",
38
+ "scored well across platform assessments", "backed by strong assessment results",
39
+ "assessments confirm solid competence",
40
  ),
41
  "reachability.full": (
42
  "easy to reach on verified channels", "fully verified contact details", "reachable via verified email, phone and LinkedIn",
43
  "verified across contact channels", "a fully verified profile", "easy to contact, all channels verified",
44
+ "every contact channel checks out", "verified end to end for outreach", "no friction reaching this candidate",
45
  ),
46
  "reachability.partial": (
47
  "reachable on verified channels", "verified email and phone on file", "contactable on verified details",
48
  "partly verified contact info", "verified on a couple of channels", "reachable, partially verified",
49
+ "some contact channels verified", "reachable, though not fully verified", "a couple of verified ways to reach them",
50
  ),
51
  "interest.high": (
52
  "drawing recruiter interest lately", "saved by recruiters recently", "getting strong recruiter views",
53
  "shortlisted by recruiters of late", "surfacing in recruiter searches", "noticed by recruiters this month",
54
+ "on recruiters' radar lately", "picking up notable recruiter attention", "seeing a spike in recruiter interest",
55
  ),
56
  "hiring.reliable": (
57
  "reliably completes interviews", "a dependable interview track record", "shows up for scheduled interviews",
58
  "consistent through hiring processes", "follows through on interviews", "a reliable hiring participant",
59
+ "follows through once an interview is scheduled", "a steady hand through hiring loops",
60
+ "consistently sees interviews to the end",
61
  ),
62
  "github.active": (
63
  "recent public GitHub activity", "active in open source lately", "visible technical work on GitHub",
64
  "an active public code footprint", "shipping in public on GitHub", "recent open-source contributions",
65
+ "recent commits visible on GitHub", "keeping an active public repo history", "visible, ongoing open-source work",
66
  ),
67
  "integrity.complete": (
68
  "a complete, detailed profile", "high profile completeness", "a thorough, well-filled profile",
69
  "a fully fleshed-out profile", "comprehensive profile detail", "a richly completed profile",
70
+ "a profile with little left blank", "thoroughly filled in across every section",
71
+ "a well-rounded, complete profile",
72
  ),
73
  "soft_pattern.active_market": (
74
  "active, open to work and responsive to recruiters", "recently active, reachable and in the market",
75
  "engaged on the platform and open to roles", "visible to recruiters and quick to respond",
76
  "in the market with strong platform engagement", "active, contactable and open to opportunities",
77
+ "clearly in the market and easy to engage", "checks every box for an active, reachable candidate",
78
+ "visibly job hunting and quick to respond",
79
  ),
80
  }
81
 
 
83
  "responsive.low": (
84
  "slow to respond to recruiters", "low recruiter responsiveness", "quiet on recruiter outreach",
85
  "infrequent platform replies", "passive on recruiter messages", "a low response rate",
86
+ "slow to reply when contacted", "rarely responds to outreach", "a quiet track record on recruiter messages",
87
  ),
88
  "reachability.minimal": (
89
  "limited verified contact info", "few verified contact channels", "thinly verified contact details",
90
  "hard to reach on verified channels", "minimal contact verification", "sparse verified contacts",
91
+ "little in the way of verified contact info", "not much verified to reach them by",
92
+ "contact details mostly unverified",
93
  ),
94
  "hiring.patchy": (
95
  "a patchy interview-attendance history", "inconsistent through interviews", "spotty interview follow-through",
96
  "an uneven hiring-process record", "misses scheduled interviews at times", "a patchy interview record",
97
+ "inconsistent about seeing interviews through", "a mixed track record in hiring processes",
98
+ "follow-through on interviews has been uneven",
99
  ),
100
  "integrity.thin": (
101
  "a thin or incomplete profile", "low profile completeness", "a sparsely filled profile",
102
  "limited profile detail", "an under-filled profile", "a bare-bones profile",
103
+ "a profile with several sections left blank", "light on profile detail overall",
104
+ "an incomplete picture from the profile alone",
105
  ),
106
  }
107
 
 
129
  )
130
 
131
  # Compact, IMPACTFUL behavior tags for the narrative sentence (distinct from the fuller SIG_BANKS clauses) —
132
+ # framed as candidate BEHAVIOR. 5 variants each for run-to-run variety. Keyed by the PRIORITY slot.
133
  BEHAVIOR_TAGS: dict[str, tuple[str, ...]] = {
134
+ "recency": ("recently active", "active on the platform", "recently engaged", "active of late",
135
+ "active within the month"),
136
+ "intent.open": ("open to work", "actively exploring roles", "in the market", "looking to move",
137
+ "job hunting"),
138
  "interest.high": ("in active recruiter demand", "drawing recruiter interest", "attracting recruiter attention",
139
+ "shortlisted by recruiters", "on recruiters' radar"),
140
  "responsive.high": ("responsive to recruiters", "quick to reply", "a responsive communicator",
141
+ "fast to respond", "easy to reach quickly"),
142
  "github.active": ("active in open source", "shipping public code", "visible in open source",
143
+ "contributing on GitHub", "keeping GitHub active"),
144
+ "assessment.high": ("assessment-validated", "strong on skill assessments", "tested well on assessments",
145
+ "backed by strong assessments", "assessment scores stand out"),
146
+ "availability.relocate": ("open to relocating", "willing to relocate", "geographically flexible",
147
+ "up for a move", "flexible on location"),
148
+ "availability.hybrid": ("set for hybrid", "hybrid-ready", "aligned to hybrid work",
149
+ "comfortable with hybrid", "geared for a hybrid setup"),
150
+ "hiring.reliable": ("reliable through interviews", "dependable in hiring", "consistent in the process",
151
+ "follows through reliably", "steady through the hiring process"),
152
+ "reachability.full": ("easy to reach", "fully reachable", "readily contactable",
153
+ "reachable without friction", "verified across every channel"),
154
+ "reachability.partial": ("reachable on verified channels", "contactable on verified details",
155
+ "reachable on some verified channels", "partly verified for contact",
156
+ "contactable, partially verified"),
157
+ "integrity.complete": ("a complete, detailed profile", "thoroughly filled out", "a well-developed profile",
158
+ "a fully rounded profile", "little left incomplete"),
159
+ "notice.short": ("available soon", "able to start quickly", "ready to move quickly",
160
+ "quick to onboard", "free to start soon"),
161
  }
162
 
163
  # Lead-ins for the behavior sentence when the candidate is broadly active in the market (≥2 strong flags).
src/rank/explain/tiers.py CHANGED
@@ -159,10 +159,10 @@ def polish(
159
  # pulled from the evidence is allowed, an invented one is not.
160
  source = f"{nlg} {evidence} {requirements}"
161
  grounded = _is_grounded_blurb(cleaned, source)
162
- # _SYSTEM asks for "70 to 85 words", which routinely runs 500-600+ chars for real prose (technical
163
- # terms + connectives) — a 500 cap silently discarded well-formed, grounded notes for no real reason
164
- # (rank 1 in one run: grounded, 3 sentences, third-person, 535 chars → rejected on length alone).
165
- ok_length = 120 <= len(cleaned) <= 600
166
  multi_sentence = len(_SENTENCE_END.findall(cleaned)) >= 3
167
  third_person = not _FIRST_PERSON.search(cleaned) # reject verbatim first-person copies of the summary
168
  if grounded and ok_length and multi_sentence and third_person:
 
159
  # pulled from the evidence is allowed, an invented one is not.
160
  source = f"{nlg} {evidence} {requirements}"
161
  grounded = _is_grounded_blurb(cleaned, source)
162
+ # No upper bound: a fixed char cap silently discarded well-formed, grounded notes for no real reason
163
+ # (rank 1 in one run: grounded, 3 sentences, third-person, 535 chars → rejected by a since-removed 500
164
+ # cap). ``max_tokens`` (generation-time) is the real ceiling; a lower floor still screens out junk.
165
+ ok_length = len(cleaned) >= 120
166
  multi_sentence = len(_SENTENCE_END.findall(cleaned)) >= 3
167
  third_person = not _FIRST_PERSON.search(cleaned) # reject verbatim first-person copies of the summary
168
  if grounded and ok_length and multi_sentence and third_person: