- 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…
Run JD parse sequentially; reduce parse_max_tokens
Hemprasad Badgujar
1e50702
JD parse and preprocess were running concurrently via ThreadPoolExecutor, but both independently claim the full compute.cpu_threads budget (llama.cpp n_threads, Polars POLARS_MAX_THREADS). On constrained hardware (2 vCPUs), this causes thread contention rather than overlap — measured runs show JD parse itself ~20s slower (95.9s→116.0s) when concurrent, netting 5s worse total than sequential.
Reduce parse_max_tokens from 1536→1024 based on observed peak usage (~800 tokens); richest response (32 skills) stayed well under 1024, keeping ~28% headroom while bounding rare runaway costs.
Net effect: 57.8s end-to-end on local hardware (previously 118.1s).
Reduce top_n defaults for embedding & cross-encoder
Hemprasad Badgujar
e5d6da4
Lower composite_narrow.top_n (500 → 250) and precision.top_n (300 → 150) to reduce inference time and avoid wall-clock budget overruns when running with n_threads=2 (granite_precision ~0.74s/row, cross_encoder ~0.6s/row). Added explanatory comments noting performance measurements and headroom relative to ensemble.top_n=100. Files changed: config/settings.yaml, src/config.py.
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.
Switch JD-parse/T1 polish to Qwen3-1.7B
Hemprasad Badgujar
baeef08
Replace references and defaults for the JD-parse / T1 reasoning polish from Qwen3.5-2B to Qwen3-1.7B across docs, config, Dockerfile and code. Make T1 LLM polish conditional on reasoning.mode == "llm" (default "template" keeps full NLG, no LLM). Update model repo/gguf names, README benchmarking notes, prefetch logging, and explanatory comments to reflect the new preferred model and behavior.
Add LLM call timeout and streaming pipeline phases
Hemprasad Badgujar
26f86bb
Implement hard per-call timeout (45s) via threading to prevent runaway LLM generations that were observed taking 1996s with no way to interrupt. Convert pipeline phases to generators to yield results as they complete instead of batching at the end, enabling live status updates on slow hardware. Add per-call timing metrics to JD parse artifact. Introduce post-hoc reasoning audit to catch empty/duplicate/leaked-name issues for hackathon grading. Update polish acceptance gate with sentence-count bounds (2-4) and contradiction markers to ensure top-10 notes read as strengths-only.
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.
Reuse Qwen3.5 for T2; drop LFM2.5 & factor ONNX
Hemprasad Badgujar
190f0fc
Remove the LFM2.5/T2 polish and related SmolLM2 fallback; T1 and T2 now reuse the Qwen3.5-2B JD-parse model. Update configs, comments and README to reflect the smaller prefetch set and adjusted baked size (~2.3 GB). Factor ONNX session loading into a new src/retrieve/onnx.py and update granite/reranker to use it. Remove orjson-backed JSON/text atomic writers and their exports, simplify models/llm API (drop polish-related helpers), and clean up imports/logging. Add presentation (docs/Redrob_Submission_SoloLeveling.pptx) and various doc/UI text updates to match the new model policy.
Reasoning now follows the candidate-ranking research: outcomes over
pedigree, per-requirement justification, beyond keywords, bias-free,
concise. Driven by a prompt-engineering sweep (A/B/C/D over cached
candidates): variant C (structured, verb-first, NO few-shot) won at
11/15 grounded vs 2/15 baseline; few-shot leaked its own facts and
crashed grounding to 0/15.
- Feed the candidate's OWN WORDS (summary + career descriptions) to the
LLM so it can surface a real achievement; grounding source widened to
cover that evidence.
- Winning prompt: S1 starts with a past-tense outcome verb, S2 ties to a
stated requirement, S3 covers seniority/availability + <=1 soft gap;
third person, no schools, no dashes, specific not generic.
- Deterministic "The candidate" strip -> verb-first openers; third-person
guard rejects verbatim first-person copies; smart-quote/dash normalize.
- T2 routed to Qwen3.5-2B (LFM2.5 verified useless: 3/15 verbatim copies)
and dropped from prefetch (-220 MB).
…
Two deployment modes now work together:
- Pre-download (build): rank.py --setup bakes all 5 models into the image
- Auto-download (runtime): HF_HUB_OFFLINE left unset so a missing weight
fetches on first use (safety net); opt into strict offline with
`docker build --build-arg OFFLINE=1`
Fix offline loading: llama.cpp Llama.from_pretrained always lists the repo
over the network (even for a cached file), so strict offline broke. Pin
EXACT gguf filenames (Qwen3.5-2B-Q4_K_M.gguf, LFM2.5-350M-Q4_K_M.gguf) and
load via hf_hub_download + Llama(model_path=...), which is cache-first and
needs no Hub API call. Verified: all 5 models load with HF_HUB_OFFLINE=1.
- .dockerignore: exclude data/artifacts (build downloads fresh; lean
context mirrors a HF Space build instead of shipping the local cache)
- README: deployment section documents pre-download + auto-download
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).
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…
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.