The sift was feeding Opus a magnitude-blind, under-trusted, truncated shortlist — only 68% of the genuinely-best jobs reached the curator. Fixes: - embed.py: embed the FULL description (was desc[:600]) + role_category + industry; richer profile text (current_role + experience summary + seniority). The best semantic signal, fed real content. - sift.py: magnitude-preserving min-max fusion (was rank-position, which flattened cosine 0.95 vs 0.72) and embedding-LED weights (W_VIBE 0.6 / W_WHITEBOX 0.4). Embed runs FIRST so the white-box f_semantic reuses the real cosine (threaded as job["_vibe_cosine"]) instead of token overlap. - config.py: SIFT_TOP_K 18 → 28 (the gate was cutting good jobs before Opus). - curate.py: richer Opus briefs — full responsibilities (was desc[:500]) + role_category/industry/ seniority/pay, so even the shortlist is fully described. Regression (120 pairs): sift membership recall 0.68 → 0.92, score MAE 24.7 → 18.4. 31 tests pass.
61 lines
2.6 KiB
Python
61 lines
2.6 KiB
Python
"""The sift — combine the two cheap rankers (white-box ‖ embedding vibe) by weighted-average
|
|
RANK fusion and return the **top-K candidate pool** for the curator (Opus).
|
|
|
|
This is NOT the final cut — it only decides which ~18 jobs the expensive brain reads (retrieve cheap,
|
|
rerank expensive). Each returned job already carries its white-box `match` block (the templated
|
|
fallback the curator's output later replaces).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from app.config import get_settings
|
|
from app.engine import embed as _embed
|
|
from app.engine import rank as _rank
|
|
|
|
W_WHITEBOX, W_VIBE = 0.4, 0.6 # embedding (the truer semantic signal) now LEADS the noisy checklist
|
|
|
|
|
|
def _minmax(score_by_id: dict[str, float]) -> dict[str, float]:
|
|
"""id → score min-max-normalized into [0,1]. PRESERVES MAGNITUDE (a much-better job stays much
|
|
better) — unlike rank-position fusion, which flattened cosine 0.95 and 0.72 to the same gap."""
|
|
vals = score_by_id.values()
|
|
lo, hi = min(vals), max(vals)
|
|
rng = hi - lo
|
|
return {i: ((s - lo) / rng if rng else 1.0) for i, s in score_by_id.items()}
|
|
|
|
|
|
def sift(prefs: dict, ctx: dict | None, jobs: list[dict], k: int | None = None):
|
|
"""Returns (top_k_jobs, debug). top_k_jobs are ranked best-first; each keeps its white-box match.
|
|
|
|
Order matters: embed FIRST so the white-box `f_semantic` can reuse the real cosine (via the
|
|
threaded `_vibe_cosine`) instead of crude token overlap. Then fuse by magnitude, embedding-led."""
|
|
s = get_settings()
|
|
k = k or s.SIFT_TOP_K
|
|
if not jobs:
|
|
return [], {"mode": "empty", "scored": 0, "k": 0}
|
|
|
|
# 1. embedding cosine FIRST — thread onto each job so rank.f_semantic reads the real signal.
|
|
vibe = _embed.vibe_scores(prefs, ctx, jobs) # id → cosine, or None
|
|
if vibe:
|
|
for j in jobs:
|
|
if j["id"] in vibe:
|
|
j["_vibe_cosine"] = vibe[j["id"]]
|
|
|
|
# 2. white-box scores ALL (now semantically-aware where the cosine was threaded).
|
|
ranked = _rank.rank(prefs, ctx, jobs)["opportunities"]
|
|
if not ranked:
|
|
return [], {"mode": "empty", "scored": 0, "k": 0}
|
|
wb = {j["id"]: j["matchScore"] / 100.0 for j in ranked}
|
|
|
|
# 3. magnitude-preserving, embedding-led fusion.
|
|
if vibe:
|
|
wbn = _minmax(wb)
|
|
vbn = _minmax({i: vibe[i] for i in wb if i in vibe})
|
|
fused = {i: W_WHITEBOX * wbn[i] + W_VIBE * vbn.get(i, 0.0) for i in wb}
|
|
mode = "vibe-led(magnitude)"
|
|
else:
|
|
fused = wb
|
|
mode = "whitebox-only"
|
|
|
|
top = sorted(ranked, key=lambda j: -fused[j["id"]])[:k]
|
|
return top, {"mode": mode, "scored": len(ranked), "k": len(top)}
|