"""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)}