DS DevShelfHub Projects · AI tools

Resume & Cover Letter Improver: AI Feedback With STAR Bullets and ATS Keywords

Beginner

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.

Python Streamlit Groq API pypdf

View on GitHub

Resume & Cover Letter Improver — AI feedback with STAR bullets and ATS keywords

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 extractionpypdf reads 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 buttonst.download_button lets 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:

  1. Set up the project
    • Install Python 3.10+
    • Create a folder and a virtual environment
  2. Install dependencies
    • streamlit, groq, httpx, truststore, pypdf — pin versions in requirements.txt
  3. Store your API key
    • Put GROQ_API_KEY in .streamlit/secrets.toml
    • Never hardcode it in the script
  4. Build the UI
    • st.file_uploader for the PDF — restricted to type=["pdf"]
    • st.text_area pre-filled with extracted PDF text so users can edit before sending
    • Second st.text_area for the job description
    • Two primary buttons side-by-side using st.columns
  5. Extract PDF text
    • Wrap the uploaded bytes in io.BytesIO and pass to pypdf.PdfReader
    • Concatenate page.extract_text() across all pages; cap at MAX_RESUME_CHARS to avoid very large prompts
  6. 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
  7. Display and download
    • Render both outputs with st.markdown() so headings and bullets format correctly
    • Offer a st.download_button for the cover letter ( mime="text/plain")
  8. 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.

python

# 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.SSLContext hooks into the OS certificate store—avoids TLS failures on macOS without disabling verification.
  • _groq_client() is decorated with @st.cache_resource so the HTTP client is created once per session, not on every rerun.

2. PDF extraction

  • st.file_uploader returns a UploadedFile object; io.BytesIO wraps its bytes so pypdf.PdfReader can treat it like a file handle.
  • Text from all pages is joined with newlines and sliced to MAX_RESUME_CHARS before 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 the if 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_content only 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_button streams the string directly as a .txt file 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/except and surface the raw exception message via st.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.

Browse all →