DS DevShelfHub Projects · AI tools

Resume + Job Match Optimizer

Intermediate

By DevShelfHub

Upload a resume and job description to get a fit score (0–100), keyword gap analysis, improvement suggestions with STAR rewrites, and a tailored cover letter—Python, Streamlit, and Groq.

Python Streamlit Groq API pypdf

View on GitHub ↗

Resume + Job Match Optimizer — fit score, keyword gaps, and STAR rewrites

Resume + Job Match Optimizer is an intermediate-level project that transforms job applications into a data-driven workflow. Upload a resume (or paste the text) alongside a job description, then choose from three actions: score the fit, get structured improvement suggestions, or generate a tailored cover letter.

The fit scorer returns a 0–100 match score with a breakdown of present keywords, missing keywords, top strengths, and top gaps. The improvement flow forces the model to produce exactly four labeled sections including rewritten bullet points in STAR format. The cover letter path generates a structured 3-4 paragraph letter and offers a download button so you can send it immediately.

Built with Python, Streamlit, Groq API, and pypdf, this project demonstrates how structured prompting and multi-action UI design turn a single LLM call into a focused career tool.

Purpose: practice structured prompting and multi-button Streamlit layouts that branch into distinct LLM calls from a shared input.

Typical use: paste your resume, paste a job posting, click “Score Fit & Analyze” to see the keyword gap, then “Generate Cover Letter” and download the draft.

Key ideas you will touch:

  • PDF extraction — pypdf reads uploaded resumes without any cloud parser.
  • Fit scoring — a structured system message that forces a numeric score, keyword lists, and gap analysis in one response.
  • Three-button branching — shared validation then separate prompt logic per action, avoiding duplicated API calls.
  • Download button — st.download_button streams the cover letter as a .txt file with no server storage.

Overall flow

User uploads PDF or pastes resume + pastes job description
      ↓
Shared validation (resume required; JD required for score and cover letter)
      ↓
Button 1: Score Fit & Analyze       Button 2: Suggest Improvements      Button 3: Generate Cover Letter
      ↓                                        ↓                                      ↓
Score 0–100 + keywords +             Strengths + improvements +             3-4 paragraph letter
strengths + gaps                     STAR rewrites + ATS keywords           + Download button

Demo


Step-by-Step Implementation

  1. Set up the project
    • Install Python 3.10+, create a folder and virtual environment
  2. Install dependencies
    • streamlit, groq, httpx, truststore, pypdf
  3. Store your API key
    • GROQ_API_KEY in .streamlit/secrets.toml
  4. Build the two-column layout
    • Left column: PDF uploader + text area pre-filled with extracted text
    • Right column: job description text area
    • Three action buttons in a row below both columns
  5. Extract PDF text
    • Wrap uploaded bytes in io.BytesIO, pass to pypdf.PdfReader, concatenate all pages
    • Cap at MAX_RESUME_CHARS to keep prompts manageable
  6. Craft the three prompts
    • Score: system message forces numeric score, keyword lists, strengths, and gaps in labeled sections
    • Improve: system message forces four sections including STAR-rewritten bullet examples and ATS keywords
    • Cover letter: specifies hook, experience match, achievements with numbers, and call-to-action — output letter text only
  7. Display and download
    • All outputs render via st.markdown()
    • Cover letter adds st.download_button with mime="text/plain"
  8. Guardrails
    • Resume always required; job description required only for score and cover letter
    • Validation runs once before any API call to avoid redundant checks

Code implementation

python

# Resume + Job Match Optimizer — Streamlit + Groq
# Save as app.py, add GROQ_API_KEY to .streamlit/secrets.toml, then: streamlit run app.py

import io
import ssl

import httpx
import streamlit as st
import truststore

MAX_RESUME_CHARS = 8000
MAX_JD_CHARS = 5000

st.set_page_config(page_title="Resume + Job Match Optimizer", layout="wide")
st.title("Resume + Job Match Optimizer")
st.caption(
    "Upload or paste your resume and a job description to get a fit score, "
    "keyword gap analysis, improvement suggestions, and a tailored cover letter."
)

_ssl_ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)


@st.cache_resource
def _groq_client():
    import groq

    return groq.Groq(
        api_key=st.secrets["GROQ_API_KEY"],
        http_client=httpx.Client(verify=_ssl_ctx),
    )


_MODEL = "llama-3.3-70b-versatile"

# ── Inputs ─────────────────────────────────────────────────────────────────────
col_left, col_right = st.columns(2)

with col_left:
    st.subheader("Your Resume")
    resume_file = st.file_uploader("Upload PDF", type=["pdf"], key="resume_pdf")
    resume_default = ""
    if resume_file:
        import pypdf

        reader = pypdf.PdfReader(io.BytesIO(resume_file.read()))
        resume_default = "\n".join(
            (p.extract_text() or "") for p in reader.pages
        )[:MAX_RESUME_CHARS]
    resume_text = st.text_area(
        "Or paste resume text",
        value=resume_default,
        height=280,
        placeholder="Paste your resume here…",
    )

with col_right:
    st.subheader("Job Description")
    jd_text = st.text_area(
        "Paste the job description",
        height=280,
        placeholder="Paste the job posting here…",
    )

st.divider()

btn_col1, btn_col2, btn_col3 = st.columns(3)
do_score = btn_col1.button("Score Fit & Analyze", type="primary", use_container_width=True)
do_improve = btn_col2.button("Suggest Improvements", use_container_width=True)
do_cover = btn_col3.button("Generate Cover Letter", use_container_width=True)

# ── Shared validation ──────────────────────────────────────────────────────────
if do_score or do_improve or do_cover:
    if not resume_text.strip():
        st.warning("Please upload or paste your resume.")
        st.stop()
    if not jd_text.strip() and (do_score or do_cover):
        st.warning("A job description is required for scoring and cover letter generation.")
        st.stop()

# ── Score & keyword analysis ───────────────────────────────────────────────────
if do_score:
    system_msg = (
        "You are an expert career coach and ATS specialist. Analyze the resume against "
        "the job description and respond in this exact format:\n\n"
        "## Fit Score: [0-100]/100\n\n"
        "## Why This Score\n[2-3 sentences]\n\n"
        "## Keywords Present\n[comma-separated list]\n\n"
        "## Missing Keywords\n[comma-separated list of keywords from JD not in resume]\n\n"
        "## Top 3 Strengths\n- ...\n\n"
        "## Top 3 Gaps\n- ..."
    )
    user_msg = (
        f"RESUME:\n{resume_text[:MAX_RESUME_CHARS]}\n\n"
        f"JOB DESCRIPTION:\n{jd_text[:MAX_JD_CHARS]}"
    )
    with st.spinner("Analyzing fit…"):
        try:
            resp = _groq_client().chat.completions.create(
                model=_MODEL,
                messages=[
                    {"role": "system", "content": system_msg},
                    {"role": "user", "content": user_msg},
                ],
            )
            st.markdown(resp.choices[0].message.content)
        except Exception as exc:
            st.error(f"Error: {exc}")

# ── Improvement suggestions ────────────────────────────────────────────────────
if do_improve:
    system_msg = (
        "You are a senior resume reviewer. Analyze the resume and return exactly these sections:\n\n"
        "## Strengths\n[3-5 bullet points]\n\n"
        "## Suggested Improvements\n[5-7 specific, actionable bullet points]\n\n"
        "## Rewritten Bullet Examples\n[Take 2-3 weak bullets and rewrite them with metrics using STAR format]\n\n"
        "## ATS Keywords to Add\n[comma-separated list]"
    )
    user_msg = f"RESUME:\n{resume_text[:MAX_RESUME_CHARS]}"
    if jd_text.strip():
        user_msg += f"\n\nJOB DESCRIPTION (for context):\n{jd_text[:MAX_JD_CHARS]}"

    with st.spinner("Reviewing resume…"):
        try:
            resp = _groq_client().chat.completions.create(
                model=_MODEL,
                messages=[
                    {"role": "system", "content": system_msg},
                    {"role": "user", "content": user_msg},
                ],
            )
            st.markdown(resp.choices[0].message.content)
        except Exception as exc:
            st.error(f"Error: {exc}")

# ── Cover letter ───────────────────────────────────────────────────────────────
if do_cover:
    system_msg = (
        "You are an expert career coach. Write a tailored cover letter for this candidate. "
        "Structure: opening hook that mentions the specific role, 1-2 paragraphs matching "
        "experience to the job requirements with specific achievements and numbers, "
        "a brief paragraph about why this company, and a confident call-to-action closing. "
        "Output only the letter text — no subject line, no meta-commentary. Keep it under 400 words."
    )
    user_msg = (
        f"RESUME:\n{resume_text[:MAX_RESUME_CHARS]}\n\n"
        f"JOB DESCRIPTION:\n{jd_text[:MAX_JD_CHARS]}"
    )
    with st.spinner("Writing cover letter…"):
        try:
            resp = _groq_client().chat.completions.create(
                model=_MODEL,
                messages=[
                    {"role": "system", "content": system_msg},
                    {"role": "user", "content": user_msg},
                ],
            )
            letter = resp.choices[0].message.content
            st.markdown("### Your Cover Letter")
            st.markdown(letter)
            st.download_button(
                "Download Cover Letter (.txt)",
                data=letter,
                file_name="cover_letter.txt",
                mime="text/plain",
            )
        except Exception as exc:
            st.error(f"Error: {exc}")

Complete code — Project link (GitHub)


📖 How the Code Works (Step-by-Step)


1. Two-column input layout

  • st.columns(2) places the resume and job description panels side-by-side on wide screens and stacks them on mobile.
  • The resume text area is pre-filled with extracted PDF text, so users can edit the content before sending it to the model.

2. Shared validation before branching

  • One if do_score or do_improve or do_cover: block runs shared checks so you don’t repeat the same resume-empty guard in three places.
  • The job description gate fires only for the paths that require it, keeping the improve flow usable without a job posting.

3. Fit score prompt design

  • The system message specifies every section heading and format (e.g., ## Fit Score: [0-100]/100), preventing the model from producing a narrative paragraph instead of the structured output the UI expects.
  • Both resume and job description are injected as labeled blocks in the user message so the model treats them as separate inputs.

4. Cover letter prompt

  • The system message explicitly says “output only the letter text — no subject line, no meta-commentary” to stop the model prefixing “Here is your cover letter:”.
  • st.download_button passes the string directly as data—no temp file, no server storage.
Browse all →