All articles
Build Guides

Build a Resume Tailoring Agent with Groq and Llama 3 (Free Tier)

FDE Coach EditorialAugust 29, 202610 min read

What We're Building

We're building a local web app that takes your stale base resume and a fresh job description, then outputs a brutally optimized, ATS-friendly rewrite. No more manually tweaking bullet points for an hour. This agent uses Groq's free-tier inference running Llama 3 to restructure your experience section, inject missing keywords, and tune the skills list—all while preserving your actual experience truthfully.

Feature set:

  • Upload a PDF or DOCX resume and extract raw text.
  • Paste a target job description.
  • One-click generation of a rewritten resume section with optimized bullet points.
  • ATS keyword gap analysis showing what was added.
  • Copy-to-clipboard output so you can paste straight into your doc.

This isn't a generic ChatGPT wrapper. We're engineering specific prompt chains, structured output parsing, and a clean Streamlit UI that you can share with non-technical friends.

Architecture & Data Flow

The flow is linear but we're doing real engineering at each step. The resume parser handles two binary formats. The prompt assembly isn't just string concatenation—we're structuring a system prompt, user context, and output format instructions. The Groq API returns streaming tokens that we buffer and parse as structured JSON. Finally, the UI renders the diff and lets you iterate.

Prerequisites: The Free Toolchain

Everything here runs on free tiers. No credit card required for the core functionality.

ToolPurposeFree Tier LimitLink
GroqLlama 3 inference30 requests/min, 7k tokens/min on llama3-8b-8192console.groq.com
Python 3.10+RuntimeOpen sourcepython.org
StreamlitUI frameworkOpen sourcestreamlit.io
PyPDF2PDF text extractionOpen sourcepip install
python-docxDOCX text extractionOpen sourcepip install

You'll need a Groq API key. Sign up at console.groq.com, navigate to API Keys, and generate one. Store it as an environment variable—never hardcode keys.

Step 1: Project Setup and API Key

Create a project directory and a virtual environment. This keeps dependencies isolated.

mkdir resume-tailor && cd resume-tailor
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

Install dependencies:

pip install streamlit groq PyPDF2 python-docx

Create a .env file (add to .gitignore immediately):

GROQ_API_KEY=gsk_your_key_here

Load it in your app. Create app.py:

import os
from groq import Groq

client = Groq(api_key=os.environ.get("GROQ_API_KEY"))

Test the key works with a quick call. The free tier uses llama3-8b-8192—fast enough for interactive use and surprisingly capable for structured rewriting tasks.

Step 2: Parsing PDF and DOCX Resumes

Resumes come in two formats. We need a unified text extraction function that handles both gracefully.

import PyPDF2
import docx
import io

def extract_text_from_pdf(file_bytes: bytes) -> str:
    reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
    text = ""
    for page in reader.pages:
        extracted = page.extract_text()
        if extracted:
            text += extracted + "\n"
    return text

def extract_text_from_docx(file_bytes: bytes) -> str:
    doc = docx.Document(io.BytesIO(file_bytes))
    return "\n".join([para.text for para in doc.paragraphs])

def parse_resume(uploaded_file) -> tuple[str, str]:
    """Returns (text, file_type) given a Streamlit UploadedFile."""
    file_bytes = uploaded_file.read()
    if uploaded_file.name.endswith('.pdf'):
        return extract_text_from_pdf(file_bytes), "pdf"
    elif uploaded_file.name.endswith('.docx'):
        return extract_text_from_docx(file_bytes), "docx"
    else:
        raise ValueError("Unsupported file format. Use PDF or DOCX.")

Why this matters: PyPDF2's extract_text() can return empty strings on scanned PDFs. We handle that edge case. For DOCX, we strip paragraph-level text—tables and headers often get mangled, but for ATS keyword matching, raw text is what we need. If you hit scanned PDFs, you'd need OCR (Tesseract, free but heavy), which is a natural extension.

Step 3: The Core Prompt Engineering Logic

This is where the engineering happens. We're not just asking Llama 3 to "rewrite my resume." We're building a structured prompt that constrains the output format, enforces truthfulness, and optimizes for ATS keyword density.

SYSTEM_PROMPT = """You are an expert resume writer and ATS optimization specialist. 
Your task is to rewrite resume content to match a job description while maintaining 
100% truthfulness. You must:

1. Analyze the job description for key skills, technologies, and action verbs.
2. Rewrite each resume bullet point to incorporate missing keywords naturally.
3. Never fabricate experience—only rephrase and emphasize existing experience.
4. Add a "Skills Inferred from Context" section for keywords that align with 
   the candidate's described work but aren't explicitly listed.
5. Return ONLY valid JSON with this exact structure:
{
  "rewritten_bullets": ["bullet 1", "bullet 2", ...],
  "skills_added": ["skill1", "skill2"],
  "skills_in_context": ["skill3"],
  "match_score_estimate": 85
}
"""

def build_user_prompt(resume_text: str, job_description: str) -> str:
    return f"""### RESUME TEXT:
{resume_text[:4000]}

### JOB DESCRIPTION:
{job_description[:4000]}

Rewrite the experience bullet points and skills section. Return JSON."""

Key design decisions:

  • We truncate inputs to 4000 chars each to stay well within the 8k context window, leaving room for the response.
  • The system prompt explicitly forbids fabrication. ATS keyword stuffing with lies will get caught in interviews.
  • We request structured JSON output so we can parse it programmatically, not just display raw text.
  • The "Skills Inferred from Context" field is a clever hack: if the JD asks for "Kubernetes" and you mention "containerized microservices," the model can flag it as inferred rather than claiming you know it.

Now the API call:

import json

def tailor_resume(client: Groq, resume_text: str, job_description: str) -> dict:
    completion = client.chat.completions.create(
        model="llama3-8b-8192",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": build_user_prompt(resume_text, job_description)}
        ],
        temperature=0.3,  # Low temp for consistent, factual output
        max_tokens=2048,
        response_format={"type": "json_object"}  # Groq supports JSON mode
    )
    response_text = completion.choices[0].message.content
    return json.loads(response_text)

Why temperature=0.3: We want deterministic, factual rewrites. High temperature introduces creative hallucinations—exactly what we're avoiding. JSON mode on Groq ensures the response is parseable JSON, eliminating the "Sure, here's the JSON..." wrapper text.

Step 4: Building the Streamlit UI

Streamlit gives us a professional UI with minimal code. Here's the full app.py with state management and error handling.

import streamlit as st
import os
from groq import Groq

# Page config
st.set_page_config(page_title="Resume Tailoring Agent", page_icon="📄", layout="wide")
st.title("📄 Resume Tailoring Agent")
st.caption("Powered by Groq + Llama 3 — Free tier, no credit card needed.")

# Initialize Groq client
@st.cache_resource
def get_groq_client():
    return Groq(api_key=os.environ.get("GROQ_API_KEY"))

client = get_groq_client()

# Sidebar for inputs
with st.sidebar:
    st.header("Inputs")
    uploaded_file = st.file_uploader("Upload Resume (PDF or DOCX)", type=["pdf", "docx"])
    job_description = st.text_area("Paste Job Description", height=300, 
                                    placeholder="Paste the full job description here...")
    generate_btn = st.button("🔧 Tailor My Resume", type="primary", use_container_width=True)

# Main area
if uploaded_file and job_description:
    try:
        resume_text, file_type = parse_resume(uploaded_file)
        
        with st.expander("📋 Extracted Resume Text", expanded=False):
            st.text(resume_text[:2000])
        
        if generate_btn:
            with st.spinner("Analyzing job description and rewriting..."):
                result = tailor_resume(client, resume_text, job_description)
            
            st.success(f"Estimated ATS match score: {result.get('match_score_estimate', 'N/A')}%")
            
            col1, col2 = st.columns(2)
            with col1:
                st.subheader("🔑 Skills Added")
                for skill in result.get("skills_added", []):
                    st.markdown(f"- `{skill}`")
            with col2:
                st.subheader("🔍 Skills Inferred from Context")
                for skill in result.get("skills_in_context", []):
                    st.markdown(f"- `{skill}` ⚠️ verify")
            
            st.subheader("✍️ Rewritten Bullet Points")
            rewritten = result.get("rewritten_bullets", [])
            full_text = "\n".join([f"• {b}" for b in rewritten])
            st.text_area("Copy these into your resume", full_text, height=300)
            
            st.download_button("📥 Download as Text", full_text, file_name="tailored_resume.txt")
    
    except Exception as e:
        st.error(f"Error: {str(e)}")
        st.info("Common issues: invalid file format, Groq API rate limit, or malformed JSON response.")
else:
    st.info("👈 Upload your resume and paste a job description to get started.")

UI decisions:

  • Sidebar keeps inputs persistent while you iterate on different JDs.
  • The "Skills Inferred from Context" section has a ⚠️ marker—these need human verification before claiming.
  • Download button gives you a clean text file to paste into your actual resume document.

Step 5: Running the Agent Locally

Set your API key and launch:

export GROQ_API_KEY="gsk_your_key_here"
streamlit run app.py

Open http://localhost:8501. Upload a resume, paste a JD, click the button. First run takes ~3-5 seconds on Groq's free tier—Llama 3 8B inference is blazing fast on their LPU hardware.

Rate limit awareness: The free tier gives you 30 requests per minute. For personal use iterating on a few job applications, this is plenty. If you're building this for a career center with dozens of users, you'd need to implement queuing or upgrade.

Sensible Extensions

This agent works today, but here's where you take it next:

  1. Multi-section rewriting: Currently we focus on bullet points. Extend the prompt to rewrite the professional summary and skills section separately, each with their own JSON field.

  2. Keyword gap visualization: Use st.bar_chart or a diff library to show exactly which keywords were missing and where they were inserted. Makes the ATS optimization transparent.

  3. LaTeX resume output: If you maintain your resume in LaTeX (as many engineers do), add a Jinja2 template that injects the rewritten bullets directly into your .tex file and compiles to PDF.

  4. Job board scraper: Combine this with a scraper for LinkedIn or Indeed job listings (using the techniques from our review sentiment dashboard build) to auto-fetch JDs and batch-process applications.

  5. Multi-model comparison: Groq also serves Mixtral and Gemma models. Add a dropdown to compare rewrites from different models—useful for understanding which LLM writes most naturally for your industry.

For engineers thinking about deploying this as an internal tool at a career services company, the pattern mirrors what we discuss in scaling yourself as an FDE—build the prototype, validate with real users, then hand off the prompt engineering and API integration specs to a core team for production hardening.

Common Pitfalls and Debugging

"JSON decode error" on response: Even with JSON mode, Llama 3 sometimes adds trailing text or escapes characters incorrectly. Add a robust parser:

def safe_json_parse(response_text: str) -> dict:
    # Find the first { and last }
    start = response_text.find('{')
    end = response_text.rfind('}') + 1
    if start != -1 and end > start:
        return json.loads(response_text[start:end])
    raise ValueError("No JSON object found in response")

Rate limit 429 errors: Groq's free tier is generous but shared. Implement exponential backoff:

import time

def tailor_with_retry(client, resume, jd, max_retries=3):
    for attempt in range(max_retries):
        try:
            return tailor_resume(client, resume, jd)
        except Exception as e:
            if "429" in str(e) and attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise

Scanned PDFs return empty text: PyPDF2 can't handle image-based PDFs. If you see empty extraction, the resume was likely a scan. The fix is integrating pytesseract with pdf2image, but that's a separate build.

Hallucinated experience: If the model adds skills you don't have, your temperature is too high or your system prompt isn't strict enough. The current prompt explicitly forbids fabrication, but always manually review the "Skills Inferred from Context" section before using it.

FAQ

Is my resume data sent to Groq? Yes, the text is sent to Groq's API for inference. Groq's privacy policy states they don't train on customer data, but if you're handling highly sensitive PII, consider running a local model via Ollama instead. For most job seekers, the convenience outweighs the privacy tradeoff.

How accurate is the match score? It's an LLM estimate, not a real ATS simulation. Treat it as a directional signal. Real ATS systems use proprietary algorithms. The value is in the rewritten bullets and keyword injection, not the number.

Can I use this for multiple job applications simultaneously? The current UI processes one at a time. For batch processing, you'd refactor the Streamlit app into a script that iterates over a folder of JDs. The free tier's 30 req/min limit applies—batch responsibly.

What if I want to deploy this for my team? Streamlit Community Cloud offers free hosting for public apps. For private deployments, consider Streamlit's paid tiers or containerize with Docker and deploy on a $5 VPS. If you're an FDE building this for a customer, the LLM feature deployment case study walks through enterprise guardrails and evaluation frameworks you'll need.

Why Groq instead of OpenAI's free tier? Groq's inference speed on Llama 3 is genuinely impressive—sub-second token generation on their LPUs. For an interactive tool where users iterate on rewrites, latency matters. Plus, no credit card required, which removes friction for sharing this tool with others.

#agents#resume#groq#llama3#job-search

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now
Build a Resume Tailoring Agent with Groq and Llama 3 (Free Tier) | FDE Coach