Explainable CV screening

Seven agents.
One shortlist.
Every score explained.

HireSense scores a stack of CVs against one job description using sentence embeddings, flags biased language on both sides, redacts names before review, and attaches a reason to every ranking it produces.

Streamlit · sentence-transformers · SHAP · SQLite · runs entirely on CPU

Ranked candidate

#1 of 40

candidate_id: cv_0417.pdf

candidate_name: [REDACTED]

Match score
0.81
Persona fit
0.64
Composite
0.74

Explanation

Match to the job description increases the score by 0.14; persona fit decreases it by 0.03.

Bias flags: none detected · name redacted before review

  • 7

    pipeline agents

  • 4

    NLP models, all local

  • 384

    embedding dimensions

  • 898

    lines of Python

  • 12

    columns per candidate row

  • 0

    API keys required

The problem

A ranking nobody can explain is a ranking nobody should trust.

Screening a stack of CVs by hand is slow, and the ordering rarely comes with a reason you could defend to the candidate who didn't make it. Keyword filters make it worse: they miss good people who phrased things differently and quietly reward whoever gamed the wording.

HireSense scores on meaning rather than exact terms, surfaces biased phrasing in the job post as well as the CVs, redacts names before a human looks, and attaches an explanation to every result. Each stage leaves a CSV behind, so the ranking can be audited after the fact.

What it does

Six things every shortlist comes with.

  • Semantic CV match

    Ranks candidates by how close their CV is to the job in meaning, so good applicants aren't lost to different wording.

  • Biased-language flags

    Catches loaded terms like 'rockstar' and 'ninja' in the job post and in the CVs themselves.

  • Name redaction

    Detected personal names are replaced with [REDACTED] before anyone reviews the shortlist.

  • Persona signal

    A sentiment and soft-skills score sits alongside the raw match rather than being folded invisibly into it.

  • Per-candidate explanation

    Every ranking carries a sentence naming which feature moved the score, so no result is unexplained.

  • Inspectable at every stage

    Each of the seven stages writes a CSV you can open. Nothing about the ranking is hidden in memory.

Under the hood

Seven scripts, run in order, each one auditable on its own.

"Agent" here means a single-purpose Python script, not an autonomous model deciding what to do next. There is no LLM loop and no tool calling — the supervisor runs a fixed sequence, and each stage reads the previous stage's CSV and writes its own.

stage 2 / 7

CV grader

cv_grader.py

Extracts text from every uploaded PDF, embeds each CV and the job description as 384-dimension vectors, and ranks candidates by cosine similarity. Meaning, not keyword overlap.

Models loaded
all-MiniLM-L6-v2 · cosine similarity
Writes
cv_grading_results.csv

Three decisions worth defending

Orchestration

Agents talk through CSV files, not function calls.

The supervisor runs each of the seven agents as its own subprocess, and every handoff is a file on disk. That buys three things: any stage can be run alone from the command line, the intermediate state is inspectable by opening a spreadsheet, and a failure is localised to one script instead of one long traceback. The cost is honest and known — every run cold-loads T5, MiniLM, distilbert and spaCy from scratch, seven Python starts deep. Simplicity over speed, chosen on purpose for a build with a deadline.

supervisor.py:17-35

Preprocessing

Rewrite the job post only when it needs rewriting.

Paraphrasing every job description would be a good way to quietly corrupt half of them. Instead the optimizer computes a Flesch-Kincaid grade first and only calls T5 when the post reads above grade 10. A clearly-written job description passes through byte for byte, and the model is spent on the ones that are actually dense.

jd_optimizer.py:80-88

Retrieval

One vector per CV, and the limit that comes with it.

Each CV is embedded whole rather than chunked, and compared to a single job-description vector by brute-force cosine — no vector database, no ANN index, no top-k cutoff. At hackathon scale that is the right amount of machinery, and it keeps the scoring trivial to reason about. It also carries a real limit worth naming: MiniLM truncates at roughly 256 word-piece tokens, so anything past about half a page of CV never reaches the score.

cv_grader.py:62-63

Stack

Interface
Streamlit ≥1.32
Orchestration
Python subprocesspandas ≥2.2numpy ≥1.26
NLP
sentence-transformersall-MiniLM-L6-v2 (384-d)t5-smalldistilbert-SST2spaCy en_core_web_sm
Scoring
scikit-learn ≥1.4SHAP ≥0.44PyTorch ≥2.2
Storage
SQLitePyPDF2 ≥3.0CSV artifacts per stage

Honest limitations: the bias check is a fixed eight-word lexicon, and both it and the persona score read only the first 200 characters of each CV. Long CVs are truncated at the embedding stage. Scanned image PDFs produce no text and are skipped silently. The SHAP explainer sits over a fixed 0.6/0.4 blend, so it restates those weights rather than discovering them. Nothing here has been benchmarked for match quality or bias-detection accuracy.