- New Step 04.5 bm25_prefilter: cheap lexical BM25 pre-cut right after
hard_filter, before potion_broad's expensive embedding pass. Shrinks
potion's input from ~74K to 10K rows using JD-keyword overlap alone.
composite_narrow still runs its full potion+soft+bm25 blend later, once
those signals exist, so ranking quality is unchanged. Falls back to a
full passthrough on an empty/failed JD spec rather than an arbitrary cut.
- broad_retrieval/composite_narrow/precision all use fixed top_n counts
now (500/200/125) instead of a mix of top_pct and top_n.
- Moved the candidates Parquet cache down into load_candidates() itself
(was one layer up in run_preprocess, which Streamlit's per-phase
explorer bypasses). Now both the CLI and Streamlit share one cache —
verified Streamlit's eager warm-up dropped from 14.6s to 0.27s.
- Streamlit's _load_phase_input switched to st.cache_resource (avoids
copying the 100K-row frame on every cache hit; safe since every
downstream Polars st…
Move JD extraction from data/artifacts/ (gitignored) to data/ so the demo-critical artifact ships with the repo/image. Increase n_ctx from 4096→6144 to handle longer JDs safely. Reduce parse_attempts from 2→1 (mitigated via improved prompt clarity). Retire target_industries/exclude_companies from LLM prompt to free budget for skill/title recall, but keep as [] in schema for stability. Major prompt refinements: simplify role/task structure, emphasize skill recall priority with explicit scan guidance, consolidate constraints. Add data/jd_extraction.json fixture (parsed from sample job_description.md) and empty secrets file. Update tests & docs to reflect retired prompt fields.
Add CPU threading, T1-only LLM polish, experiment script
Hemprasad Badgujar
a41b023
Introduce process-level CPU thread budgeting and cgroup-aware resolution (config.compute.cpu_threads, resolve_cpu_threads, configure_compute, onnx_session_options). Wire cpu_threads through ONNX loaders and retrievers. Switch Step-11 LLM polish to T1-only (top 10) and update tiers/acceptance logic; add batched prompt handling and stability fixes. Add scripts/explain_prompt_experiment.py for fast prompt iteration. Improve settings loading so RERANKER_* env vars override YAML (Yaml-backed source). Minor UX/CI touches: CLI/Streamlit call order, hashing fixes in preprocess, model prefetch updates, README updates, and new runtime tests.
Explain: A+B sentence structure, behavioral framing, more variety
Hemprasad Badgujar
362838c
Reasoning rewrite per feedback — present strengths then behavior, no
template feel, no AI-looking em-dashes.
- Structure (A+B): S1 identity + fit verdict; S2 strengths (+<=1 soft gap
on T3/T4); S3 behavioral signals as a NARRATIVE (open to work, in
recruiter demand, active in open source) not a data dump; S4 why-ranked
(dominant signal + which JD fields matched lexically).
- Drop the rank ordinal ("top 10") — redundant with the rank column;
replace with concrete signal + BM25 field-hit detail.
- Variety so it doesn't read as a template: varied openers, connectives
(and / as well as), gap pivots (though / albeit / the main gap being),
and 5 behavior-sentence forms. Result: 100/100 distinct reasonings.
- No em-dashes anywhere (commas/periods/colons only).
- Grammar fixes: a/an before vowels, no doubled connectives, no
"...is that <noun phrase>", no doubled seniority ("Lead Staff").
- Length guard: trims keyword tail then strengths so every line <500.
- Location promoted in…
Rebuild Step 11 into a package (src/rank/explain/) with a three-layer
pipeline: FactEngine -> advanced rule-based NLG composer -> tiered LLM
polish. Carries ~90 of 100 finalists with zero LLM.
- bands.py: numbers -> subjective band labels (never raw floats)
- engine.py: JD-grounded FactEngine (missing must-haves, nice-to-have,
3-tier location, country, exclude-company, certs, notice/recency,
widened dominant-signal across all funnel legs)
- compose.py: clause aggregation + connectives (and/but/though), opener
variation, potion-base-32M grounded top-k variant selection, T1=4 /
others 3-4 sentences, >=6 variants/slot
- signals.py: redrob_signals behavioral bank, adaptive best-first,
sentinel skip, >=2-flag soft-pattern gate
- tiers.py: per-tier model (T1 Qwen3.5-2B reused, T2 LFM2.5-350M with
SmolLM2-360M fallback); grounding gate + over-compression guard;
Qwen3-1.7B dropped (-1.1 GB disk)
- Negatives gated to T3/T4 CSV only (top picks read positively);
explanation + exp…
Enhance JD extraction (Qwen3.5-2B + richer prompt) and Step-11 reasoning
Hemprasad Badgujar
eb18ae9
JD extraction:
- Switch JD parse to Qwen3.5-2B (standard-instruct GGUF); reasoning stays Qwen3-1.7B (separate handle)
- Gate Qwen's /no_think soft switch to Qwen models only (LlmClient._soft_no_think)
- Enrich the OBJECTIVE prompt with related skills (nice_to_have), target_industries,
exclude_companies, and all welcomed locations; guard out JD-rejected domains
- best-of-N now targets must_have recall only; _as_str_list dedups keywords case-insensitively
- City->country gazetteer fallback for acceptable_countries
Step-11 reasoning:
- Enrich grounded facts: industry / education / recency / title-fit + dominant-signal tail
- Revive the recent_days knob; LLM polish on Qwen3-1.7B
Config/cleanup: hard-filter graded checks off at funnel entry; doc fixes; remove stray files.
Tests: +dedup / related-field / explain coverage (93 pass, ruff + mypy clean).
Enrich the granite-embedded candidate text with familiar skills + pillar strengths
Hemprasad Badgujar
fb57bfa
The body granite embeds now folds in more of the parameters we compute:
- materialize adds "Also familiar with: <familiar skills>" (beginner/intermediate
skills were previously dropped from the embedded body).
- quality appends "Strengths: <strong pillars>." — the quality pillars (>=0.6) named
as words the embedder can use.
- precision.max_length 128 -> 192 so the enriched body actually lands in the embedding
window (texts are p50~308, so 128 truncated most of it).
Quality-neutral-to-better on the full 100K run (mean matched skills 4.3 vs 4.37; the
top-3 now surfaces a Search Engineer and a Pune ML Engineer — more JD-aligned for a
search/ranking role). Cost: granite 10s -> 18s, total ~77s (well under the 5-min budget).
Also harden two explain assertions against the true-random phrasing.
Enrich the embed/rerank query with extracted skills + roles
Hemprasad Badgujar
1cbb761
potion / granite / cross-encoder previously compared candidate text against only the
JD ideal_summary; the extracted must_have_skills and job_titles fed BM25 but not the
embedders. embed_query() now composes ideal_summary + "Core skills: ..." + "Relevant
roles: ..." (summary leads so it survives the embedders' 128-token truncation and
leaves the cross-encoder room for the doc; skills/titles capped 15/8). Wired into the
pipeline and the explorer (which now shows the enriched query per embed phase).
Quality-neutral on the full 100K run (mean matched skills 4.31 vs 4.37 baseline, within
temp-0.5 parse noise). Also fix a band assertion made flaky by the true-random phrasing.
Explainability: true-random phrasing + drop dead reasoning module
Hemprasad Badgujar
226492c
- Stat-bank variant selection now uses true randomness (random.choice): the subjective
explanation prose varies run to run. Ranking stays deterministic — only the text
changes, never candidate_id/rank/score — so the submission contract is unaffected.
- Delete src/reasoning.py and test_reasoning.py: add_reasoning is unreachable now that
the pipeline/explorer use add_explanation. The one reused helper (_clean_note) moves
into explain.py; a focused test covers it.
Add JD-grounded explainability for the top 100 (Step 11)
Hemprasad Badgujar
95e1a87
Replace the generic one-line reasoning with explain.py, which explains each finalist
AGAINST the JD using the spec + every score column:
- matched must_have_skills, title match, location-in-JD-cities, experience vs the
years band, and the dominant ranking signal (reranker/granite/composite/quality).
- Phrasing varies per candidate via a stat bank keyed by a candidate_id hash —
deterministic (reproducible), not random.
- Ranks 1..explain_llm_top_n get a Gemma polish when reasoning.mode == "llm"; the rest
(and any LLM failure) use the template.
Surfaces (both): submission.csv reasoning becomes JD-grounded (contract preserved), and
a full per-candidate breakdown is written to data/reports/explanations.json (matched
skills, title/location, band, score breakdown) and shown in the explorer.
Wire add_explanation into the pipeline (with the JD spec) and the explorer rank stages;
add config (explain_llm_top_n, explanations_path) and tests. reasoning.py is kept for
_clean_note/_facts_exp…
Use the JD locations field: add a field-scoped BM25 locations group
Hemprasad Badgujar
5fa38c6
Audit showed locations_preferred/acceptable was extracted from the JD but had no
downstream consumer. Add a "locations" BM25 group matching the JD cities against the
candidate's location column (soft lexical preference; the acceptable_countries hard
drop already runs separately), with a bm25_weight_locations weight. Every extracted JD
field is now consumed by the funnel.
JD prompts:
- Restructure objective/subjective prompts to Role-Task-Context-Constraints-Output;
pin the persona + Mode constraint so Qwen3 skips its thinking loop.
- Make the objective prompt GENERIC (no leaked JD-specific skill/title examples) after
experiments showed listed examples don't drive recall and can anchor/echo.
- Compress the schema 12->7 keys: merge nice_to_have into must_have_skills, merge the
two location fields, drop certifications/target_industries/exclude_companies from the
prompt (they default to [] in _coerce_objective, so the funnel is unaffected). The
leaner prompt freed model capacity -> ~30 skills recall.
- job_titles: keep the verbatim posted title first, add the role family, and always
include the JD's core-activity roles (Search/Ranking/IR Engineer). Generic framing.
- acceptable_countries inferred from cities/"outside <country>"; capture every city.
Recall controls (config):
- Best-of-N objective extraction: parse_temperature (0.5), parse_attem…
Add field-scoped BM25 and composite blending
Hemprasad Badgujar
99deeee
Implement field-scoped BM25 matching (match each JD keyword bag only against its candidate text section), z-normalize per-field scores, and blend them into the composite bm25_score used in composite_narrow.
Changes include:
- Rank logic: introduce field_scoped_bm25, _znorm, and helpers in src/rank/composite.py; define _BM25_FIELDS and use per-field bm25 weights to compute bm25_score and bm25_matched metrics; keep composite as z-blend of potion/soft/bm25.
- Config: expose per-field BM25 weights and document field-scoped behavior in src/config.py and config/settings.yaml.
- Streamlit: update explorer labels and inputs to show BM25 query terms by field, reuse field_scoped_bm25/_BM25_FIELDS for display, and add an option to regenerate JD extraction (live parse) before running the pipeline.
- README: document the field-scoped BM25 approach and show field→JD mappings and blending behavior.
- Tests: update tests/test_composite.py to exercise per-section text columns and adjust expectations f…
Drop disqualifiers field — remove dead JD signal
Hemprasad Badgujar
86788de
disqualifiers was stored on JDSpec and displayed but never consumed by
any filter (hard_filter doesn't read it). Removed it from the OBJECTIVE
prompt, _coerce_objective, JDSpec + _spec_from_mapping, the Streamlit JD
panel, and the test assertion, so there's no orphaned always-empty field.
Add reasoning LLM, perf tweaks, and UI timing
Hemprasad Badgujar
f85f907
Reduce ONNX truncation lengths to 128 for precision and rerank to cut O(n^2) cost. Add a configurable reasoning step (reasoning.mode: template|llm) with small Gemma GGUF client and token cap; implement safe LLM rephrase with cleaning and deterministic fallback. Parallelize JD parse and preprocessing to overlap model load / polars work. Make candidate location visible to BM25 and include JD locations in composite BM25 queries. Update JD parse prompt wording and add a Streamlit timing summary; tests updated to use template reasoning settings.
Add full pipeline, submission & reasoning support
Hemprasad Badgujar
e73e29e
Introduce an end-to-end pipeline and deliverable handling: add pipeline.run_full and a CLI --out flag to run the full funnel and write submission.csv. Add a deterministic reasoning step (reasoning.add_reasoning) to generate one-line recruiter blurbs and a submission writer (rank.write_submission) that normalizes scores, enforces the 100-row validator contract, and atomically writes the CSV. Move JD country hard-drop from soft_filter into composite_narrow and replace the legacy accept_outside_india boolean with an acceptable_countries list across jd.parse, jd.spec, config models, and tests. Add ReasoningSettings and SubmissionSettings to config and update settings.yaml and .gitignore. Wire reasoning into the Streamlit app, export write_submission from rank.__init__, and add tests for reasoning and submission; adjust tests that relied on the old location behavior.
Add ensemble ranking step
Hemprasad Badgujar
9fb0e9b
Introduce a new ensemble ranking stage that z-normalizes and fuses reranker, granite, and composite signals into a weighted final_score, then selects top_n and assigns a 1-based rank. Adds EnsembleSettings (defaults: top_n=100, weights 0.5/0.3/0.2, z_clip=3.0) and YAML config block, implements src/rank/ensemble.py with empty/missing-signal guards, exports ensemble from rank package, integrates it into streamlit_app pipeline, and includes unit tests covering scoring, ranking, missing signals, and empty input.
Add ONNX granite/reranker and compute config
Hemprasad Badgujar
66cf40d
Introduce ONNX-based precision embedding (granite) and cross-encoder reranker with CPU-tuned runtime settings. Adds src/common/runtime.py to set thread-pool env vars and provide onnx session options, and new retrieve modules (granite.py, reranker.py) implementing embedding and pair-scoring via onnxruntime + tokenizers. Wire up configure_compute() in entrypoints (rank.py, src/cli.py, src/streamlit_app.py), expose new retrieve functions in src/retrieve/__init__.py, and extend config (src/config.py and config/settings.yaml) with precision/rerank settings and model defaults. Reduce composite_narrow.top_n to 500 and add precision.top_n=300. Add lightweight tests for passthrough guards (tests/test_granite.py, tests/test_reranker.py) and update pyproject.toml to include onnxruntime and tokenizers dependencies.
Add retrieval & soft/composite ranking stages
Hemprasad Badgujar
08c3203
Introduce broad retrieval (potion Model2Vec) + soft_filter scoring + composite_narrow (BM25 + z-blend) pipeline steps. Adds retrieval implementations (potion static embedder, BM25 tokenizer/scorer), ranking modules (soft_filter, composite_narrow), and tests for bm25, potion, composite, soft_filter and hard_filter behavior. Update config/settings to expose new tuning blocks (broad_retrieval, soft_filter, composite_narrow) and default embedder, refine hard_filter inactive logic and integrity toggles, and export new rank/retrieve APIs. Streamlit explorer updated to include rank stages and show JD spec; pyproject dependencies added model2vec and numpy. These changes narrow the candidate pool deterministically before expensive embedding and blend semantic + lexical + quality signals for the top-N cut.
Add JD parsing, central config, checkpoints & LLM
Hemprasad Badgujar
700e896
Introduce a central Settings system (config/settings.yaml + src/config.py) and enable live JD extraction (problem/job_description.md) via a new JD parsing pipeline (src/jd/*) that emits a unified jd_extraction.json. Add a local GGUF LLM client and helper (src/models/llm.py, src/models/__init__.py) to drive the JD calls and model prefetching. Implement safe atomic file IO and per-phase Parquet checkpoints (src/common/io.py, src/common/checkpoint.py) and wire them into common exports. Expand preprocess functionality (schema overrides, date helper, pipeline exports) and update the CLI to run preprocess and accept a --checkpoints flag. Update Dockerfile to include central config and data, and refresh pyproject dependencies for the new modules. Add a suite of tests for the new components.
Ship data in image + orjson loader; fix HF file-not-found