Portfolio Projects ยท Project 5 of 10

Project 5: AI Resume Screener & Job Matcher

๐Ÿ› ๏ธ Beginnerโฑ 1โ€“2 weekends๐Ÿ“„ Resume lines included

A project every recruiter instantly understands. You'll parse resumes, extract skills into structured JSON, match them against job descriptions with explainable scores, and check the system for bias.

What you'll build

Architecture

Resume โ†’ text extraction (pdfplumber / python-docx) โ†’ LLM structured extraction (Pydantic schema)
JD โ†’ requirement extraction (must-have, nice-to-have, years)
Match = 0.5 ยท embedding similarity + 0.4 ยท must-have coverage + 0.1 ยท experience fit
      โ†’ ranked list + explanation + gaps โ†’ recruiter UI (human decides)

Tech stack

pdfplumber, python-docx, an LLM with JSON schema output, sentence-transformers, pandas, Streamlit, plus a simple bias test harness.

Step by step

  1. Define the schema: name (stored separately), skills[], total_years, roles[], education[], projects[].
  2. Extract with an LLM in structured-output mode; validate with Pydantic and retry once on failure.
  3. Normalise skills with a synonym map ("PyTorch" = "pytorch", "GenAI" โ‰ˆ "Generative AI").
  4. Parse the JD into must-have and nice-to-have skills and the years required.
  5. Score: cosine similarity of embeddings (resume summary vs JD), coverage of must-haves, and experience fit.
  6. Explain: matched skills, missing skills, evidence snippets.
  7. Bias test: remove names, photos, gender and age signals before scoring; run counterfactual swaps and check the score difference is about 0.
  8. UI: a ranked table, filters and export to CSV. The recruiter makes the decision.

Starter code

from pydantic import BaseModel
from sentence_transformers import SentenceTransformer, util
class Resume(BaseModel): skills: list[str]; total_years: float; roles: list[str]; summary: str
emb = SentenceTransformer("all-MiniLM-L6-v2")
def score(r: Resume, jd_text: str, must: list[str], years: float):
    sim = float(util.cos_sim(emb.encode(r.summary), emb.encode(jd_text)))
    have = {s.lower() for s in r.skills}; cov = sum(m.lower() in have for m in must) / max(1, len(must))
    exp_fit = min(1.0, r.total_years / max(years, 0.5))
    return round(100 * (0.5 * sim + 0.4 * cov + 0.1 * exp_fit)), [m for m in must if m.lower() not in have]

Deploy it

Streamlit on Hugging Face Spaces, using sample, synthetic resumes only. Never upload real people's resumes publicly. State in the README that the tool assists, and never replaces, human judgement.

Put it on your resume

Interview questions you'll get

Stretch goals

Created by Ajeetkumar