Engine Phase 3: non-tech scoring (role-aware weights + curator) + vocab
- rank.py: get_weights(profile) — when the profile reads non-tech (sales/finance/HR/ops/support/…), tilt the weights to role(0.16)/industry(0.12)/semantic(0.12), drop skill(0.18). Tech weights unchanged. (The competency-skill-backfill was tried + REVERTED — uniform per-role competencies added no discrimination and Phase 2's real-cosine f_semantic already supplies the non-tech signal.) - curate.py: non-tech instruction — role/responsibility fit is the spine; a missing skill list is NOT a negative; a strong role+industry+responsibility+seniority match is a genuine ~80, not ~65. - test_contracts.py: allowlist the new keyword-handled non-tech vocab (active boards keyword-fold; numeric codes deferred since the active feed/recall paths don't use them). Regression (120 pairs): MAE 18.4 → 16.1 (all from non-tech: sales 17.1→13.0, ops 16.3→11.7), recall holds 0.92, tech unchanged. 31 tests pass.
This commit is contained in:
@@ -36,6 +36,13 @@ in THIS job's actual skills/title.
|
||||
Honesty rules: scores reflect reality — a partial match is in the 60s-70s, not the 90s. Ground every note \
|
||||
in the job's real content; invent nothing. Frame as helpful guidance, not a hiring verdict.
|
||||
|
||||
NON-TECH roles (sales, marketing, finance, HR, operations, support, supply-chain, legal, admin): the SPINE \
|
||||
of the match is role/function fit + responsibility overlap + industry + seniority — NOT a skill-tag checklist. \
|
||||
These postings rarely list skills, and a missing skill list is NOT a negative signal — score on the \
|
||||
responsibilities and role alignment, never penalize the candidate for the board's empty skill field. A strong \
|
||||
role+industry+responsibility+seniority match with no listed tags is a genuine ~80 match, not a ~65. Make one \
|
||||
breakdown dimension "Role fit" (same function / adjacent / different).
|
||||
|
||||
Respond with ONLY a JSON object (no prose, no markdown fences):
|
||||
{"kept":[{"id","score","fit","archetype","one_line","breakdown":[{"name","score","level","note"}],\
|
||||
"coach_note","growth":{"text","from","to"}}]}
|
||||
|
||||
@@ -53,7 +53,7 @@ def profile_from(prefs: dict, user_context: dict | None = None) -> UserProfile:
|
||||
countries = [lp["country"].lower() for lp in locs if lp.get("country")]
|
||||
wm = [w.lower() for w in (prefs.get("workMode") or [])]
|
||||
title = C.title_of(prefs) or None
|
||||
skills = S.canon((user_context or {}).get("skills")) # empty in Phase 1 (prefs-only)
|
||||
skills = S.canon((user_context or {}).get("skills")) # absent for non-tech → semantic (cosine) carries it
|
||||
toks = {t for t in re.findall(r"[a-z0-9+#]+", f"{title or ''} {' '.join(skills)}".lower()) if len(t) > 2}
|
||||
return UserProfile(
|
||||
title=title, skills=skills, years=C.years_of(prefs),
|
||||
@@ -146,11 +146,29 @@ def f_semantic(p, job):
|
||||
FACTORS = {"skill": f_skill, "experience": f_experience, "location": f_location, "salary": f_salary,
|
||||
"industry": f_industry, "role": f_role, "semantic": f_semantic}
|
||||
|
||||
# Non-tech identity = role + industry + meaning, NOT a skill checklist (which boards rarely list).
|
||||
# When the profile reads non-tech, tilt the weights there; skill drops, semantic/role/industry rise.
|
||||
NON_TECH_FAMILIES = {"sales", "marketing", "finance", "accounting", "hr", "human resource", "operations",
|
||||
"ops", "customer support", "customer service", "bpo", "legal", "supply chain", "admin",
|
||||
"business development"}
|
||||
NONTECH_WEIGHTS = {"skill": 0.18, "experience": 0.22, "location": 0.14, "salary": 0.06,
|
||||
"industry": 0.12, "role": 0.16, "semantic": 0.12}
|
||||
|
||||
|
||||
def _is_nontech(p: UserProfile) -> bool:
|
||||
blob = " ".join([*(r.lower() for r in p.roles), (p.title or "").lower()])
|
||||
return any(fam in blob for fam in NON_TECH_FAMILIES)
|
||||
|
||||
|
||||
def get_weights(p: UserProfile) -> dict:
|
||||
return NONTECH_WEIGHTS if _is_nontech(p) else WEIGHTS
|
||||
|
||||
|
||||
def utility(p: UserProfile, job: dict):
|
||||
w = get_weights(p)
|
||||
factors = {k: fn(p, job) for k, fn in FACTORS.items()}
|
||||
num = sum(WEIGHTS[k] * v for k, v in factors.items() if v is not None)
|
||||
den = sum(WEIGHTS[k] for k, v in factors.items() if v is not None)
|
||||
num = sum(w[k] * v for k, v in factors.items() if v is not None)
|
||||
den = sum(w[k] for k, v in factors.items() if v is not None)
|
||||
U = (num / den) if den else None # NO present signals → None (filtered out)
|
||||
return U, factors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user