Resume + Job Match Optimizer
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.
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 —
pypdfreads 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_buttonstreams the cover letter as a.txtfile 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
-
Set up the project
- Install Python 3.10+, create a folder and virtual environment
-
Install dependencies
-
streamlit,groq,httpx,truststore,pypdf
-
-
Store your API key
-
GROQ_API_KEYin.streamlit/secrets.toml
-
-
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
-
Extract PDF text
-
Wrap uploaded bytes in
io.BytesIO, pass topypdf.PdfReader, concatenate all pages - Cap at
MAX_RESUME_CHARSto keep prompts manageable
-
Wrap uploaded bytes in
-
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
-
Display and download
- All outputs render via
st.markdown() -
Cover letter adds
st.download_buttonwithmime="text/plain"
- All outputs render via
-
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
# 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_buttonpasses the string directly asdata—no temp file, no server storage.