Resume & Cover Letter Improver: AI Feedback With STAR Bullets and ATS Keywords
By DevShelfHub
Beginner-friendly project: resume plus job description for AI feedback (strengths, gaps, STAR bullets, ATS keywords) and a tailored cover letter—Python, Streamlit, Groq API, and pypdf.
Resume & Cover Letter Improver is a beginner-friendly project that transforms the job application process into a fast, structured workflow. Users can upload a resume (or paste text) along with a job description, and the system uses AI to generate actionable feedback and tailored content.
The tool analyzes the resume to highlight strengths, identify gaps, suggest improvements, rewrite bullet points using the STAR format, and extract ATS-friendly keywords. It can also generate a customized cover letter aligned with the target role, ready to download and use instantly.
Built using Python, Groq API, and pypdf, this project demonstrates how large language models can be applied to real-world career tools. It focuses on structured prompt design, document parsing, and generating role-specific professional outputs in a single automated pipeline.
Purpose: practice passing a structured prompt to an LLM and handling binary file uploads—two skills that show up constantly in real AI projects.
Typical use: paste last week’s resume into the left panel, paste a job listing into the right panel, click “Suggest Improvements” to get a reviewer’s eye view, then click “Generate Cover Letter” and edit the draft before sending.
Key ideas you will touch:
-
PDF text extraction —
pypdfreads uploaded files without any external service. - Structured prompting — a system message that forces the model to reply with exactly four labeled sections (Strengths, Improvements, Rewritten Bullets, ATS Keywords).
- Context injection — the job description is appended to the prompt only when present, making it optional for the feedback path and required for the cover letter path.
-
Download button —
st.download_buttonlets users save the generated letter without any server-side file storage.
Privacy note: resume text is sent to the Groq API. Do not use confidential content unless your policy allows it.
Overall flow
User uploads PDF or pastes resume text
↓
User pastes job description (optional for feedback, required for cover letter)
↓
Clicks "Suggest Improvements" Clicks "Generate Cover Letter"
↓ ↓
Groq analyzes resume Groq writes 3-4 paragraph letter
↓ ↓
Structured feedback on screen Letter on screen + Download button
Step-by-Step Implementation
Follow these steps in order:
-
Set up the project
- Install Python 3.10+
- Create a folder and a virtual environment
-
Install dependencies
-
streamlit,groq,httpx,truststore,pypdf— pin versions inrequirements.txt
-
-
Store your API key
-
Put
GROQ_API_KEYin.streamlit/secrets.toml - Never hardcode it in the script
-
Put
-
Build the UI
-
st.file_uploaderfor the PDF — restricted totype=["pdf"] -
st.text_areapre-filled with extracted PDF text so users can edit before sending - Second
st.text_areafor the job description - Two primary buttons side-by-side using
st.columns
-
-
Extract PDF text
-
Wrap the uploaded bytes in
io.BytesIOand pass topypdf.PdfReader -
Concatenate
page.extract_text()across all pages; cap atMAX_RESUME_CHARSto avoid very large prompts
-
Wrap the uploaded bytes in
-
Craft the prompts
- Feedback system message: instruct the model to produce exactly four labeled sections — Strengths, Improvements, Rewritten Bullet Examples, ATS Keywords
- Cover letter system message: specify structure (hook, experience match, two achievements with numbers, call to action) and output format (letter text only, 3-4 paragraphs)
- Append the job description to the user message when present
-
Display and download
-
Render both outputs with
st.markdown()so headings and bullets format correctly -
Offer a
st.download_buttonfor the cover letter (mime="text/plain")
-
Render both outputs with
-
Guardrails
- Warn when resume is empty; warn when cover letter is requested without a job description
- Wrap API calls in
try/except
Code implementation
The single app.py below handles PDF extraction, prompt construction,
and both output modes. Add GROQ_API_KEY to
.streamlit/secrets.toml and install from
requirements.txt before running.
# Resume & Cover Letter Improver — 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 = 4000
st.set_page_config(page_title="Resume & Cover Letter Improver", layout="centered")
st.title("Resume & Cover Letter Improver")
st.caption(
"Upload or paste your resume, add a job description, and get AI-powered "
"feedback plus 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(timeout=120.0, verify=_ssl_ctx),
)
_CHAT_MODEL = "llama-3.3-70b-versatile"
def _extract_pdf_text(uploaded_file) -> str:
import pypdf
reader = pypdf.PdfReader(io.BytesIO(uploaded_file.read()))
return "\n".join(page.extract_text() or "" for page in reader.pages)
st.markdown("### Your Resume")
uploaded = st.file_uploader("Upload PDF resume", type=["pdf"])
resume_from_pdf = ""
if uploaded:
with st.spinner("Extracting text from PDF…"):
try:
resume_from_pdf = _extract_pdf_text(uploaded)[:MAX_RESUME_CHARS]
st.success(f"Extracted {len(resume_from_pdf):,} characters from PDF.")
except Exception as err:
st.error(f"Could not read PDF: {err}")
pasted_resume = st.text_area(
"Or paste resume text here",
value=resume_from_pdf,
height=240,
placeholder="Paste your resume content here…",
max_chars=MAX_RESUME_CHARS,
)
final_resume = (pasted_resume or resume_from_pdf).strip()
st.markdown("### Job Description")
job_desc = st.text_area(
"Paste the job description (required for Cover Letter; optional for Improvements)",
height=160,
placeholder="Copy and paste the full job listing you're applying for…",
max_chars=MAX_JD_CHARS,
)
st.markdown("---")
col_a, col_b = st.columns(2)
with col_a:
do_improve = st.button("Suggest Improvements", type="primary", use_container_width=True)
with col_b:
do_cover = st.button("Generate Cover Letter", type="primary", use_container_width=True)
if do_improve or do_cover:
if not final_resume:
st.warning("Please upload or paste your resume first.")
elif do_cover and not job_desc.strip():
st.warning("Please paste a job description to generate a tailored cover letter.")
else:
client = _groq_client()
if do_improve:
with st.spinner("Analyzing your resume…"):
try:
system_msg = (
"You are an expert resume reviewer and career coach. "
"Review the resume and provide exactly four sections using these headings:\n"
"**Strengths** — 2-3 bullets on what is done well.\n"
"**Improvements** — 3-5 specific, actionable fixes.\n"
"**Rewritten Bullet Examples** — rewrite 2 weak bullet points using the STAR method.\n"
"**ATS Keywords** — list 8-10 keywords missing from the resume common for this role. "
"Be direct and constructive. Do not add any preamble."
)
user_content = f"Resume:\n{final_resume}"
if job_desc.strip():
user_content += f"\n\nTarget job description:\n{job_desc}"
resp = client.chat.completions.create(
model=_CHAT_MODEL,
messages=[
{"role": "system", "content": system_msg},
{"role": "user", "content": user_content},
],
)
feedback = (resp.choices[0].message.content or "").strip()
st.markdown("### Resume Feedback")
st.markdown(feedback)
except Exception as err:
st.error(f"Something went wrong: {err}")
if do_cover:
with st.spinner("Writing your cover letter…"):
try:
system_msg = (
"You are a professional cover letter writer. "
"Write a compelling, tailored cover letter based on the resume and job description. "
"Structure: strong opening hook, connect experience to the role's requirements, "
"highlight 2-3 specific achievements with numbers where possible, close with a call to action. "
"Keep it to 3-4 paragraphs. "
"Use the candidate's name from the resume if present; otherwise write in first person. "
"Output only the letter text — no instructions or meta commentary."
)
resp = client.chat.completions.create(
model=_CHAT_MODEL,
messages=[
{"role": "system", "content": system_msg},
{
"role": "user",
"content": (
f"Resume:\n{final_resume}\n\nJob description:\n{job_desc}"
),
},
],
)
letter = (resp.choices[0].message.content or "").strip()
st.markdown("### 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 err:
st.error(f"Something went wrong: {err}")
Complete code — Project link (GitHub)
Demo
📖 How the Code Works (Step-by-Step)
A walkthrough of app.py from top to bottom.
1. Page setup and SSL
-
truststore.SSLContexthooks into the OS certificate store—avoids TLS failures on macOS without disabling verification. -
_groq_client()is decorated with@st.cache_resourceso the HTTP client is created once per session, not on every rerun.
2. PDF extraction
-
st.file_uploaderreturns aUploadedFileobject;io.BytesIOwraps its bytes sopypdf.PdfReadercan treat it like a file handle. -
Text from all pages is joined with newlines and sliced to
MAX_RESUME_CHARSbefore pre-filling the text area—the user sees and can edit what will actually be sent.
3. Two-button layout
-
Both buttons live in a
st.columns(2)row; clicking either one re-runs the Streamlit script, so theif do_improve or do_cover:block handles shared validation before branching. - Only the relevant spinner, API call, and output block runs for the clicked button.
4. Feedback prompt
- The system message names all four output sections in the instruction so the model cannot omit or rename them— predictable structure makes it easier to parse or post-process later.
-
The job description is appended to
user_contentonly when non-empty, giving the model context for ATS keyword suggestions without making the field mandatory.
5. Cover letter prompt
- The system message specifies the exact structure—hook, experience match, achievements with numbers, call to action—and tells the model to output only letter text, preventing meta-commentary like “Here is your cover letter:”.
-
st.download_buttonstreams the string directly as a.txtfile with no temporary file on disk.
6. Error handling
- Input guards run before any network call: empty resume triggers a warning; missing job description blocks the cover letter path only.
-
Both API calls are wrapped in
try/exceptand surface the raw exception message viast.error()without crashing Streamlit.
Tips & Production Considerations
Always paste the full job description
The ATS keyword analysis is only as good as the JD you provide. Pasting the full posting (not just the title) lets the model surface niche skills and industry-specific terms that a generic resume review would miss. If the JD is behind a login wall, copy the rendered text rather than the URL.
Treat STAR bullets as a starting point
The model invents plausible metrics when your resume omits them. Always verify numbers (percentages, team sizes, dollar amounts) before using a rewritten bullet. Replace placeholders with real data from your experience; recruiters follow up on specific claims.
PDF text extraction can lose formatting
pypdf extracts text layer only. If your resume uses images, complex
tables, or unusual fonts, some content may be garbled or missing. Check the extracted preview before running
analysis. For best results, use a resume built from a standard Word or LaTeX template.
Run multiple passes for different roles
The same resume can score very differently against two job descriptions. Run the tool once per role you are applying to and save each output. Comparing the keyword gaps across roles reveals which skills to emphasize on a master resume versus a tailored version.
Keep sensitive data local
Your resume contains personal information (name, email, phone, address). The app sends this text to Groq for processing. If privacy is a concern, redact contact details before pasting, or run the app against a local model via Ollama so nothing leaves your machine.
Resume & Cover Letter Improver FAQ
What is the Resume & Cover Letter Improver?
The Resume & Cover Letter Improver is a beginner-friendly Streamlit app that ingests a resume and a job description, then returns AI feedback—strengths, gaps, STAR-format bullet rewrites, ATS keyword coverage, and a tailored cover letter—using the Groq API.
Is the Resume & Cover Letter Improver free to use?
The source code is free and open on GitHub. You only pay for Groq API usage. A single resume + JD analysis with cover letter typically costs less than a cent on Groq's free tier.
What tech stack does the Resume & Cover Letter Improver use?
Python and Streamlit for the UI, pypdf for extracting text from PDF resumes, and the Groq Python client for chat completions. No vector store or RAG pipeline is required—the resume and JD fit in a single prompt.
How does ATS keyword analysis work in the Resume & Cover Letter Improver?
The system prompt asks the model to list the most important keywords pulled from the job description, then check which appear in the resume and which are missing. Missing keywords are surfaced as a concrete checklist you can add to bullets without keyword-stuffing.
What is a STAR bullet in the Resume & Cover Letter Improver?
STAR stands for Situation, Task, Action, Result—a structured format recruiters scan for. The app rewrites weak resume bullets into STAR form (e.g., "Led a 4-engineer migration that reduced p95 latency by 38%") so each bullet has measurable impact.
What are alternatives to the Resume & Cover Letter Improver?
For a numeric fit score between resume and JD, see the Resume + Job Match Optimizer in the same repo. For general product copy and benefit-led bullets, see the AI Product Description Generator.