File size: 9,299 Bytes
ae7dc3e
95e1a87
 
 
 
 
 
 
 
 
 
 
ae7dc3e
 
 
 
95e1a87
 
ae7dc3e
 
 
eb18ae9
95e1a87
 
 
ae7dc3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb18ae9
ae7dc3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362838c
ae7dc3e
 
 
 
 
 
 
 
a41b023
362838c
 
ae7dc3e
 
 
 
 
 
 
eb18ae9
 
ae7dc3e
 
 
 
226492c
 
ae7dc3e
 
 
 
95e1a87
ae7dc3e
 
 
 
 
 
95e1a87
 
ae7dc3e
 
95e1a87
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
"""Step 11 multi-band explainability β€” bands, FactEngine matchers, NLG composition, tier gap-gating, artifact."""

from __future__ import annotations

from pathlib import Path

import orjson
import polars as pl

from config import ReasoningSettings, Settings
from jd import JDSpec
from rank import add_explanation
from rank.explain import _clean_note, _is_grounded, _matched_skills, _title_match
from rank.explain.bands import experience_fit, location_tier, notice_band, recency_band, skill_coverage
from rank.explain.engine import matched_education, matched_industries, title_fit
from rank.explain.signals import signal_bands

_SPEC = JDSpec(
    must_have_skills=["Python", "embeddings", "ranking"], nice_to_have=["RAG", "HNSW"],
    job_titles=["AI Engineer", "Search Engineer"], locations_preferred=["Pune", "Noida"],
    locations_acceptable=["Bangalore"], years_min=5, years_max=9,
    target_industries=["Fintech", "Healthcare"], education_fields=["Computer Science"],
)


def _row(**over: object) -> dict[str, object]:
    base = {
        "candidate_id": "C1", "current_title": "Senior AI Engineer", "years_of_experience": 7.4,
        "experience_band": "senior", "location": "Pune, MH", "country": "India",
        "current_industry": "Fintech", "current_company": "Acme",
        "flat_skills": ["Python", "embeddings", "ranking"], "flat_career_industries": ["Fintech"],
        "flat_certifications": [], "education": [{"field_of_study": "Computer Science"}],
        "days_since_active": 10, "notice_period_days": 15, "is_available_soon": True,
        "open_to_work_flag": True, "willing_to_relocate": False, "preferred_work_mode": "hybrid",
        "verification_count": 3, "recruiter_response_rate": 0.8, "saved_by_recruiters_30d": 6,
        "profile_views_received_30d": 25, "interview_completion_rate": 0.9, "profile_completeness_score": 85,
        "has_github": True, "github_score_clean": 70.0, "avg_skill_assessment": 75.0, "verified_skills": ["Python"],
        "bm25_hit_skills": True, "bm25_hit_titles": True, "bm25_hit_education": True,
        "bm25_hit_locations": True, "bm25_hit_industries": True,
        "potion_score": 0.9, "soft_score": 0.8, "bm25_score": 0.7, "composite_score": 0.85,
        "granite_score": 0.9, "reranker_score": 1.9, "candidate_quality": 0.7, "final_score": 1.0,
    }
    base.update(over)
    return base


def _frame(rows: list[dict[str, object]]) -> pl.DataFrame:
    return pl.DataFrame(rows)


def _settings(tmp_path: Path, **reasoning: object) -> Settings:
    opts = {"mode": "template", "variant_select": "random",
            "explanations_path": str(tmp_path / "explanations.json")}
    opts.update(reasoning)
    return Settings(reasoning=ReasoningSettings(**opts))


# ── band mappers ──────────────────────────────────────────────────────────────────────────────────────────────
def test_band_mappers() -> None:
    assert experience_fit(7.0, 5, 9) == "in" and experience_fit(3.0, 5, 9) == "below"
    assert experience_fit(11.0, 5, 9) == "above"
    assert skill_coverage(3, 3) == "strong" and skill_coverage(1, 3) == "partial" and skill_coverage(0, 3) == "thin"
    assert location_tier("Pune", ["Pune"], ["Bangalore"]) == "preferred"
    assert location_tier("Bangalore", ["Pune"], ["Bangalore"]) == "acceptable"
    assert location_tier("Berlin", ["Pune"], ["Bangalore"]) == "outside"
    assert recency_band(10, 90) == "month" and recency_band(60, 90) == "quarter" and recency_band(400, 90) == "stale"
    assert notice_band(15, False) == "short" and notice_band(90, False) == "long"


def test_fact_matchers() -> None:
    assert _matched_skills(["Python", "Go"], ["python", "sql"]) == ["Python"]
    assert _title_match(["AI Engineer"], "Senior AI Engineer") == "AI Engineer"
    assert title_fit(["AI Engineer"], "Senior AI Engineer", True) == ("AI Engineer", "align")
    # skills match but title totally off β†’ mismatch trap
    assert title_fit(["AI Engineer"], "Accountant", True) == ("AI Engineer", "mismatch")
    assert matched_industries(["Fintech"], "FinTech Solutions", ["SaaS"]) == ["Fintech"]
    assert matched_education(["Computer Science"], [{"field_of_study": "Computer Science"}]) == ["Computer Science"]


def test_signal_bands_sentinel_and_gate() -> None:
    # github sentinel (has_github False) β†’ skipped; active_market gate fires (open + active + responsive + interest)
    sig = signal_bands(_row(has_github=False), recency="month", location_outside=False)
    assert sig["github"] is None
    assert sig["active_market"] is True and sig["reachability"] == "full"
    # passive candidate: not enough flags β†’ no soft pattern
    passive = signal_bands(_row(open_to_work_flag=False, recruiter_response_rate=0.2, saved_by_recruiters_30d=0,
                                profile_views_received_30d=0), recency="stale", location_outside=False)
    assert passive["active_market"] is False


# ── composed output ───────────────────────────────────────────────────────────────────────────────────────────
def test_grounded_multisentence_reasoning(tmp_path: Path) -> None:
    out = add_explanation(_frame([_row()]), _SPEC, _settings(tmp_path))
    top = out.row(0, named=True)
    assert "Python" in top["reasoning"] and "Pune" in top["reasoning"]      # skills + location surfaced
    assert top["reasoning"].count(".") >= 3                                 # multi-sentence (T1 ~ 4)
    assert "AI Engineer" in top["explanation"]                              # title detail in the fuller note


def test_t1_has_no_gap_but_explanation_does(tmp_path: Path) -> None:
    # a candidate with a real gap (missing skill + outside location), forced into T1 (rank 1)
    row = _row(flat_skills=["Python"], location="Berlin, DE", country="Germany")
    out = add_explanation(_frame([row]), _SPEC, _settings(tmp_path, tier1_top=1))
    top = out.row(0, named=True)
    assert "though" not in top["reasoning"].lower() and "outside" not in top["reasoning"].lower()  # T1 = clean
    assert "Watch-outs:" in top["explanation"]                             # gaps live in the explanation/audit


def test_t3_surfaces_one_soft_gap(tmp_path: Path) -> None:
    # two rows; tier bounds put rank 2 into T3 (allow_neg) β€” a gap candidate there shows ≀1 soft gap in CSV
    rows = [_row(candidate_id="A"), _row(candidate_id="B", flat_skills=["Python"], location="Berlin, DE",
                                         days_since_active=400, reranker_score=0.1, final_score=0.1)]
    out = add_explanation(_frame(rows), _SPEC, _settings(tmp_path, tier1_top=1, tier2_top=1, tier3_top=2))
    b = out.filter(pl.col("candidate_id") == "B").row(0, named=True)
    pivots = ("though", "albeit", "caveat", "watch-out", "however", "main gap")  # covers every _GAP_PIVOT phrase
    hits = sum(b["reasoning"].lower().count(p) for p in pivots)
    assert hits == 1                                                        # exactly one soft gap (any pivot wording)


# ── salvage + grounding gates (unchanged behavior) ────────────────────────────────────────────────────────────
def test_clean_note_salvages_and_rejects() -> None:
    assert _clean_note("Sure! Here it is: A senior engineer with deep retrieval experience.", "fb") \
        == "A senior engineer with deep retrieval experience."
    assert _clean_note("Recruiter Note: /no_think", "fallback line here") == "fallback line here"


def test_is_grounded() -> None:
    source = "Senior AI Engineer; strong on embeddings, Pinecone, Weaviate; based in Pune."
    assert _is_grounded("Senior AI Engineer strong on embeddings and Pinecone, based in Pune.", source)
    assert not _is_grounded("Senior AI Engineer strong on Kubernetes.", source)  # invented skill rejected


# ── artifact + empty guard ────────────────────────────────────────────────────────────────────────────────────
def test_artifact_schema(tmp_path: Path) -> None:
    settings = _settings(tmp_path)
    add_explanation(_frame([_row()]), _SPEC, settings)
    payload = orjson.loads((tmp_path / "explanations.json").read_bytes())
    entry = payload[0]
    assert entry["rank"] == 1 and entry["tier"] == "T1"
    assert entry["matched_skills"] == ["Python", "embeddings", "ranking"]
    assert "bands" in entry and "signals" in entry and "positives" in entry and "sentences" in entry
    assert entry["bm25_hits"]["skills"] is True
    assert entry["llm_polished"] is False  # template mode never polishes


def test_empty_passthrough(tmp_path: Path) -> None:
    out = add_explanation(_frame([_row()]).head(0), _SPEC, _settings(tmp_path))
    assert out.height == 0 and {"reasoning", "explanation"} <= set(out.columns)