All articles
Build Guides

Build a Resume Tailoring Agent with Hugging Face Free Inference & Streamlit

FDE Coach EditorialAugust 22, 202610 min read

What We’re Building

A single-page Streamlit app that acts as your personal resume tailoring agent. You paste your base resume (plain text) and a target job description. The agent uses Mistral 7B via the Hugging Face Inference API free tier to rewrite your bullet points—preserving factual content while aligning verbs, keywords, and quantified results to the JD.

Core features:

  • Free LLM inference—no GPU, no credits expiring
  • Side-by-side diff view: original vs. tailored bullets
  • One-click copy for the rewritten sections
  • Fully local Python runtime; your data never hits a third-party cloud outside the HF API call
  • Prompt engineered to maintain truthfulness (no hallucinated metrics)

If you’ve ever wondered how forward deployed engineers prototype AI features for enterprise hiring tools in a single afternoon, this is the pattern. For a deeper look at the role, see Demand for Forward Deployed Engineers: Why This Role Is Booming.

Architecture & Data Flow

The flow is linear but opinionated: we extract bullets client-side before sending anything to the model. This keeps the LLM focused on rewriting discrete units rather than free-forming a full document—critical for avoiding hallucinations. The prompt constructor injects the JD’s key themes and a strict truthfulness constraint.

Prerequisites & Free Tier Setup

Everything here is free forever or has a generous free tier:

Important: The free Inference API is rate-limited (~30k characters/month). For personal use, this is plenty. If you hit the limit, wait an hour or switch to a locally-run model with Ollama—the code pattern stays identical.

Step 1: Project Scaffold & Dependencies

Create a project directory and a virtual environment:

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

Install dependencies:

pip install streamlit huggingface-hub

That’s it. Two libraries. The huggingface-hub package gives us the InferenceClient that handles the REST calls.

Create three files:

resume-tailor/
├── app.py          # Streamlit UI
├── tailor.py       # Core logic: parse, prompt, call HF
└── .env            # HF_TOKEN (optional, or use Streamlit secrets)

Step 2: Hugging Face Inference Client

Open tailor.py. We’ll build a thin wrapper around the free inference endpoint:

import os
from huggingface_hub import InferenceClient

# Use env var or Streamlit secrets. For local dev, export HF_TOKEN="hf_..."
HF_TOKEN = os.environ.get("HF_TOKEN")
if not HF_TOKEN:
    raise RuntimeError("Set HF_TOKEN environment variable")

client = InferenceClient(
    model="HuggingFaceH4/zephyr-7b-beta",
    token=HF_TOKEN,
)

def query_model(prompt: str, max_tokens: int = 1024) -> str:
    """Send a prompt to the free HF Inference API and return the response."""
    response = client.text_generation(
        prompt,
        max_new_tokens=max_tokens,
        temperature=0.3,          # Low temp keeps output grounded
        do_sample=True,
        return_full_text=False,   # We only want the generated part
    )
    return response.strip()

Why Zephyr-7B? It’s a Mistral 7B fine-tune that excels at instruction following and is available on the free tier. It respects format constraints better than base Mistral for this structured rewriting task.

Step 3: Resume Parsing Logic

We need to extract bullet points reliably. Most resumes use , -, *, or numbered lines. Add to tailor.py:

import re

def extract_bullets(text: str) -> list[str]:
    """Extract bullet-point lines from resume text."""
    lines = text.split('\n')
    bullets = []
    bullet_pattern = re.compile(r'^\s*[•\-\*\d+\.]\s+')
    
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue
        # Match common bullet markers
        if bullet_pattern.match(stripped):
            # Remove the marker
            content = re.sub(r'^\s*[•\-\*\d+\.]\s+', '', stripped)
            if len(content) > 15:  # Filter noise like "-" alone
                bullets.append(content)
    
    return bullets

def extract_skills_section(text: str) -> str:
    """Heuristic: grab the Skills/Core Competencies block."""
    # Simple approach: find "Skills" heading, take everything until next heading or double newline
    pattern = r'(?i)(?:skills|core competencies|technical skills)[:\s]*(.*?)(?:\n\n|\n[A-Z][A-Z\s]+\n|$)'
    match = re.search(pattern, text, re.DOTALL)
    if match:
        return match.group(1).strip()
    return ""

This parser is intentionally simple. In production you’d use a library like pyresparser, but for a free agent that runs in-browser, regex gets the job done without adding weight.

Step 4: The Prompt Engineering Engine

The prompt is where the magic happens. We need the model to:

  1. Preserve every factual claim (numbers, dates, company names)
  2. Reword action verbs to match the JD’s language
  3. Inject relevant keywords naturally—never fabricate metrics
  4. Output ONLY the rewritten bullets, one per line

Add to tailor.py:

def build_tailoring_prompt(bullets: list[str], job_description: str, skills: str) -> str:
    """Construct a strict instruction prompt for bullet rewriting."""
    bullets_text = "\n".join(f"- {b}" for b in bullets)
    
    prompt = f"""<|system|>
You are an expert resume writer. Your task is to rewrite resume bullet points to match a specific job description.

RULES:
1. Preserve ALL numbers, percentages, dollar amounts, dates, and company names exactly.
2. Do NOT invent new metrics or achievements.
3. Reword action verbs and phrasing to align with the job description's language.
4. Naturally incorporate relevant keywords from the job description.
5. Maintain the original bullet point's meaning and factual content.
6. Output ONLY the rewritten bullets, one per line, with no numbering or prefixes.
7. If a bullet already matches well, return it unchanged.
</|system|>

<|user|>
Job Description:
{job_description}

Current Skills Section:
{skills}

Resume Bullet Points to Rewrite:
{bullets_text}

Rewrite each bullet point following the rules above. Output only the rewritten bullets, one per line:
</|user|>

<|assistant|>
"""
    return prompt

def tailor_bullets(bullets: list[str], job_description: str, skills: str) -> list[str]:
    """Run the full tailoring pipeline on a list of bullets."""
    if not bullets:
        return []
    
    prompt = build_tailoring_prompt(bullets, job_description, skills)
    response = query_model(prompt, max_tokens=1500)
    
    # Parse response: split by newline, clean up
    tailored = []
    for line in response.split('\n'):
        cleaned = line.strip()
        # Remove any bullet markers the model might have added
        cleaned = re.sub(r'^[•\-\*\d+\.]\s+', '', cleaned)
        if cleaned and len(cleaned) > 15:
            tailored.append(cleaned)
    
    return tailored

Critical detail: The <|system|> / <|user|> / <|assistant|> tokens are Zephyr’s chat template. Using them correctly dramatically improves instruction adherence. If you swap models later, adjust the template.

Step 5: Building the Streamlit UI

Now app.py—the frontend. We want a clean, two-column layout:

import streamlit as st
from tailor import extract_bullets, extract_skills_section, tailor_bullets

st.set_page_config(page_title="Resume Tailoring Agent", layout="wide")
st.title("🎯 Resume Tailoring Agent")
st.caption("Paste your resume and a job description. The agent rewrites your bullets using Mistral 7B—free.")

col1, col2 = st.columns(2)

with col1:
    st.subheader("📄 Your Base Resume")
    resume_text = st.text_area(
        "Paste full resume text here",
        height=400,
        placeholder="John Doe\nSoftware Engineer\n\n- Built REST APIs serving 10k req/s\n- Led migration from monolith to microservices..."
    )

with col2:
    st.subheader("💼 Target Job Description")
    jd_text = st.text_area(
        "Paste the job description",
        height=400,
        placeholder="We're looking for a backend engineer with experience in distributed systems..."
    )

if st.button("✨ Tailor My Resume", type="primary", use_container_width=True):
    if not resume_text or not jd_text:
        st.warning("Please paste both your resume and the job description.")
    else:
        with st.spinner("Extracting bullets and rewriting..."):
            bullets = extract_bullets(resume_text)
            skills = extract_skills_section(resume_text)
            
            if not bullets:
                st.error("No bullet points detected. Make sure your resume uses •, -, or * markers.")
            else:
                tailored = tailor_bullets(bullets, jd_text, skills)
                st.session_state["original_bullets"] = bullets
                st.session_state["tailored_bullets"] = tailored
                st.success(f"Rewrote {len(tailored)} bullet points!")

Step 6: Wiring the Tailoring Logic

Below the button in app.py, add the diff view that renders when results are in session state:

if "original_bullets" in st.session_state and "tailored_bullets" in st.session_state:
    st.divider()
    st.subheader("📊 Side-by-Side Diff")
    
    orig = st.session_state["original_bullets"]
    tail = st.session_state["tailored_bullets"]
    
    # Pad to same length
    max_len = max(len(orig), len(tail))
    orig += [""] * (max_len - len(orig))
    tail += [""] * (max_len - len(tail))
    
    diff_col1, diff_col2 = st.columns(2)
    
    with diff_col1:
        st.markdown("**Original Bullets**")
        for i, bullet in enumerate(orig):
            if bullet:
                st.info(f"{bullet}")
    
    with diff_col2:
        st.markdown("**Tailored Bullets**")
        for i, bullet in enumerate(tail):
            if bullet:
                changed = bullet != orig[i]
                if changed:
                    st.success(f"{bullet}")
                else:
                    st.text(f"{bullet}")
    
    # Copy button
    tailored_text = "\n".join(f"• {b}" for b in tail if b)
    st.download_button(
        label="📋 Copy Tailored Bullets",
        data=tailored_text,
        file_name="tailored_bullets.txt",
        mime="text/plain",
    )

This gives you a recruiter-ready comparison. The green highlight on changed bullets makes it obvious what was rewritten.

How to Run & Test

From your project directory:

export HF_TOKEN="hf_your_token_here"  # Or set in .env and source it
streamlit run app.py

Open http://localhost:8501. Paste a sample resume and JD, click the button, and watch the agent work.

Test case: Use a generic bullet like “Managed a team of engineers” against a JD asking for “led cross-functional engineering squads.” The agent should output “Led a cross-functional team of engineers” without fabricating headcount.

If you get an authentication error, double-check your token has read access and hasn’t expired. Free tokens don’t expire, but you might need to regenerate if you lost it.

Sensible Extensions

Once the core loop works, here’s where you can take it:

  1. Multi-model toggle – Add a dropdown to switch between mistralai/Mistral-7B-Instruct-v0.2, meta-llama/Llama-2-7b-chat-hf, or google/flan-t5-xxl. All have free inference. Adjust the chat template per model.
  2. PDF/DOCX upload – Use PyPDF2 and python-docx (both free) to parse uploaded files instead of requiring copy-paste.
  3. ATS keyword scoring – After tailoring, run the JD through sklearn.feature_extraction.text.CountVectorizer (free) and show a match percentage against the tailored bullets.
  4. Batch processing – Queue multiple JDs and generate tailored versions for each, outputting to a zip file.

For a deeper dive into shipping AI features fast, read Case Study: Deploying an LLM Feature at an Enterprise Customer in 6 Days as an FDE.

Common Pitfalls & Fixes

PitfallWhy It HappensFix
Model hallucinates metricsTemperature too high or prompt doesn’t constrainSet temperature=0.1-0.3, add explicit “DO NOT invent numbers” rule
Bullets come back with prefixesZephyr sometimes adds - or 1.Strip markers in the parser (already in the code above)
Rate limit 429 errorsFree tier caps ~30k chars/minImplement exponential backoff: time.sleep(2 ** retry)
Empty responsePrompt exceeds model’s context window (4k tokens)Chunk bullets into groups of 5-8 and process sequentially
Skills section not detectedRegex too brittle for your resume formatAdd a manual “Skills” text area as fallback input

FAQ

Q: Is my resume data safe? A: The resume text is sent to Hugging Face’s inference servers. Their free API logs prompts for abuse monitoring but doesn’t use your data for training. For sensitive documents, run the model locally with Ollama—the query_model function swaps out transparently.

Q: Can I use this for non-English resumes? A: Mistral 7B handles major European languages decently. For best results, write the system prompt in the target language. Zephyr was trained primarily on English, so expect some degradation.

Q: The output doesn’t match my writing style. A: Add a style constraint to the system prompt: “Match the original bullet’s sentence structure and tone. Use first-person if the original does.” You can also provide a sample of your writing as a few-shot example.

Q: How do I handle resumes without bullet points? A: The extract_bullets function looks for markers. For paragraph-style resumes, you’ll need to split on sentences or use a summarization approach. Consider adding a checkbox for “Paragraph-style resume” that chunks by sentence instead.

Q: What if I want to tailor the entire resume, not just bullets? A: The architecture supports it—remove the bullet extraction step and send the full resume text. However, this increases hallucination risk significantly. The bullet-by-bullet approach is a deliberate engineering trade-off for accuracy.


This agent follows the same rapid-prototyping pattern FDEs use when embedding AI into customer workflows. If you’re preparing for interviews in this space, check out The FDE Interview Loop and How to Prepare for the Technical and Stakeholder Rounds.

#resume#career#huggingface#automation

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