All articles
Build Guides

Build a Resume Tailoring Agent That Rewrites Your CV for a Specific Job Description Using Gemini

FDE Coach EditorialJuly 18, 202610 min read

What We’re Building

A command-line agent that takes your base resume (in a structured format) and a job description, then uses Google’s Gemini API to rewrite your bullet points to match the job’s keywords and seniority signals. The output is a clean, ATS-friendly LaTeX file that gets compiled directly to PDF. No GPT wrappers, no paid tiers—just Python, Gemini’s free quota, and a local LaTeX distribution.

This is a real engineering tool, not a toy. You control the prompt, the formatting, and the mapping between your raw experience and the final PDF. If you’ve ever manually tweaked bullet points for an hour before hitting submit, this agent cuts that to under 30 seconds.

Feature List

  • Structured resume input via YAML—separation of content from presentation
  • Job description ingestion from plain text or a URL
  • Gemini-powered bullet rewriting with explicit ATS-keyword injection
  • LaTeX template that produces a one-page, modern, two-column PDF
  • Free-tier operation from end to end (Gemini’s free quota gives you 60 requests per minute)
  • Extensible skill-matching via Hugging Face’s free Inference API for semantic similarity

Architecture

The Python orchestrator is the brain. It reads your base resume, extracts the job description, and for each experience block fires a carefully engineered prompt to Gemini. The response is parsed and injected into a LaTeX template that prioritizes ATS readability—no multi-column headers, no icons, no tables that confuse parsers. The Hugging Face Inference API is optional but adds a nice layer: it scores how well your existing skills match the job’s requirements, so the agent knows which skills to emphasize.

Prerequisites

Everything here is free or has a generous free tier:

Step 1: Project Scaffolding and Dependencies

Create a project directory and a virtual environment:

mkdir resume-agent && cd resume-agent
python -m venv venv
source venv/bin/activate  # or venv\Scripts\activate on Windows

Install the required packages:

pip install google-generativeai pyyaml jinja2 requests

Project structure:

resume-agent/
├── base_resume.yaml      # Your raw resume data
├── job_description.txt   # Paste the JD here
├── template.tex.j2       # Jinja2 LaTeX template
├── agent.py              # Main orchestrator
└── output/

Step 2: Configuring the Gemini Free Tier

Create a .env file (or export the variable directly):

export GEMINI_API_KEY="your-key-here"

In agent.py, initialize the client:

import os
import google.generativeai as genai

genai.configure(api_key=os.environ["GEMINI_API_KEY"])
model = genai.GenerativeModel("gemini-1.5-flash")

The gemini-1.5-flash model is fast, free, and handles the structured output we need. For more complex rewrites you can swap to gemini-1.5-pro but it counts against the same free quota.

Step 3: Ingesting and Parsing the Base Resume

Your base resume lives in base_resume.yaml. This format separates content from presentation, which makes it easy to feed structured data into the LLM:

name: "Alex Chen"
title: "Forward Deployed Engineer"
email: "alex@example.com"
phone: "+1-555-0123"
linkedin: "linkedin.com/in/alexchen"
summary: "FDE with 4 years of experience shipping enterprise AI solutions on-prem and in cloud."
skills:
  - Python
  - Kubernetes
  - Postgres
  - Terraform
  - React
experience:
  - company: "Acme Corp"
    role: "Senior FDE"
    dates: "2022–Present"
    bullets:
      - "Led on-prem deployments of ML pipelines for 3 Fortune 500 customers"
      - "Reduced time-to-value from 6 weeks to 10 days by building Terraform modules"
  - company: "StartupAI"
    role: "Solutions Engineer"
    dates: "2020–2022"
    bullets:
      - "Built customer-facing dashboards in React and FastAPI"
      - "Managed 15+ proof-of-concept engagements concurrently"
education:
  - school: "UC Berkeley"
    degree: "B.S. Computer Science"
    year: 2020

Load it in Python:

import yaml

with open("base_resume.yaml", "r") as f:
    resume = yaml.safe_load(f)

Step 4: The Core Tailoring Agent

The agent does two things: extracts keywords from the job description, then rewrites each experience block’s bullets to align with those keywords while preserving factual accuracy.

First, read the job description:

with open("job_description.txt", "r") as f:
    job_description = f.read()

Now the key function—this is where the prompt engineering lives:

def tailor_bullets(resume_block: dict, job_desc: str) -> list[str]:
    prompt = f"""
You are an expert resume writer optimizing for Applicant Tracking Systems (ATS).

Given the following job description and a candidate's original experience bullet points, rewrite each bullet to:
1. Incorporate keywords and phrases from the job description naturally.
2. Quantify impact where possible (use the original context, do not fabricate numbers).
3. Keep each bullet under 180 characters.
4. Use strong action verbs.
5. Return ONLY a valid JSON array of strings, no other text.

Job Description:
{job_desc}

Original Bullets for role {resume_block['role']} at {resume_block['company']}:
{json.dumps(resume_block['bullets'])}

Rewritten Bullets (JSON array):
"""
    response = model.generate_content(prompt)
    # Gemini sometimes wraps JSON in markdown fences; strip them
    raw = response.text.strip().removeprefix("```json").removesuffix("```").strip()
    return json.loads(raw)

Then apply it across all experience entries:

import json

tailored_experience = []
for exp in resume["experience"]:
    new_bullets = tailor_bullets(exp, job_description)
    tailored_experience.append({**exp, "bullets": new_bullets})

Optional: Hugging Face Skill Matching

If you want the agent to automatically surface which of your skills are most relevant, use the free sentence-transformers/all-MiniLM-L6-v2 model via the Inference API:

import requests

HF_TOKEN = os.environ.get("HF_TOKEN")
API_URL = "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2"
headers = {"Authorization": f"Bearer {HF_TOKEN}"}

def score_skills(skills: list[str], job_desc: str) -> dict[str, float]:
    payload = {
        "inputs": {
            "source_sentence": job_desc,
            "sentences": skills
        }
    }
    resp = requests.post(API_URL, headers=headers, json=payload)
    scores = resp.json()
    return {skill: score for skill, score in zip(skills, scores)}

This returns a similarity score per skill, which you can use to reorder the skills section in the LaTeX output.

Step 5: Generating the ATS-Optimized LaTeX

We use a Jinja2 template (template.tex.j2) that renders a clean, one-page resume. ATS parsers choke on multi-column layouts, images, and fancy fonts, so we keep it brutally simple:

\documentclass[11pt]{article}
\usepackage[margin=0.75in]{geometry}
\usepackage{enumitem}
\usepackage{hyperref}
\setlength{\parindent}{0pt}
\pagestyle{empty}

\begin{document}
\begin{center}
{\Large \textbf{ {{ name }} }}\\
{{ title }} \textbar{} {{ email }} \textbar{} {{ phone }}\\
\href{https://{{ linkedin }}}{ {{ linkedin }} }
\end{center}

\section*{Summary}
{{ summary }}

\section*{Skills}
{% for skill in skills %}{{ skill }}{% if not loop.last %}, {% endif %}{% endfor %}

\section*{Experience}
{% for exp in experience %}
\textbf{ {{ exp.role }} } \hfill {{ exp.dates }}\\
\textit{ {{ exp.company }} }
\begin{itemize}[nosep, leftmargin=*]
{% for bullet in exp.bullets %}
  \item {{ bullet }}
{% endfor %}
\end{itemize}
\smallskip
{% endfor %}

\section*{Education}
{% for edu in education %}
\textbf{ {{ edu.degree }} } \hfill {{ edu.year }}\\
{{ edu.school }}
{% endfor %}
\end{document}

Render it in Python:

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("."))
template = env.get_template("template.tex.j2")

latex_content = template.render(
    name=resume["name"],
    title=resume["title"],
    email=resume["email"],
    phone=resume["phone"],
    linkedin=resume["linkedin"],
    summary=resume["summary"],
    skills=resume["skills"],  # optionally reorder based on HF scores
    experience=tailored_experience,
    education=resume["education"]
)

os.makedirs("output", exist_ok=True)
with open("output/resume.tex", "w") as f:
    f.write(latex_content)

Step 6: Compiling to PDF

Call pdflatex from Python. This assumes pdflatex is on your PATH:

import subprocess

result = subprocess.run(
    ["pdflatex", "-output-directory=output", "output/resume.tex"],
    capture_output=True,
    text=True
)

if result.returncode != 0:
    print("LaTeX compilation failed:")
    print(result.stderr)
else:
    print("PDF generated: output/resume.pdf")

Run it twice if you want the table of contents and references to resolve (not needed for this simple template, but good practice).

How to Run the Agent

  1. Drop your base resume into base_resume.yaml
  2. Paste the job description into job_description.txt
  3. Export your keys: export GEMINI_API_KEY=... (and optionally HF_TOKEN=...)
  4. Run: python agent.py
  5. Open output/resume.pdf

The whole pipeline takes about 8–15 seconds depending on Gemini’s latency. You can iterate by tweaking the prompt in tailor_bullets() or adjusting the LaTeX template.

Extensions

  • Batch mode: Point the agent at a directory of job descriptions and generate a tailored PDF for each one. This is how you apply to 20 roles in 10 minutes.
  • Cover letter generation: Add a second Jinja2 template and a prompt that generates a three-paragraph cover letter from the same job description. If you’ve built an email triage agent before, the pattern is similar—check out our guide on building a Gmail AI triage agent with Gemini and Groq.
  • ATS scoring feedback loop: Use a free ATS simulator (like Jobscan’s free tier) to score the output, then feed the score back into the prompt as a constraint.
  • Multi-language resumes: Swap the LaTeX template for one that supports Unicode and pass a language parameter to Gemini. The architecture doesn’t change.
  • RAG over your past projects: If you keep a portfolio of project write-ups, you can wire up a retrieval step that pulls relevant projects into the resume. The pattern is identical to our RAG chatbot guide using Qdrant’s free tier.

Common Pitfalls

  • Gemini returns markdown-wrapped JSON: The model sometimes wraps the JSON array in ```json fences. The removeprefix/removesuffix chain handles this, but if you get parse errors, print raw to debug.
  • LaTeX special characters in bullet points: If your bullet points contain &, %, $, _, or #, they’ll break compilation. Escape them before rendering: bullet.replace("&", "\\&").replace("%", "\\%") etc.
  • Hugging Face cold starts: The free Inference API puts models to sleep after inactivity. The first request may time out; retry once with a 30-second timeout.
  • Gemini rate limiting: The free tier allows 60 RPM. If you’re batch-processing, add a 1-second sleep between calls.
  • Over-tailoring: If the prompt is too aggressive, the agent might inject keywords that don’t reflect your actual experience. Always review the output before sending. The agent is an accelerator, not a replacement for your judgment.

FAQ

Q: Why LaTeX instead of a Word doc? ATS parsers handle LaTeX-generated PDFs better than Word exports because the text layer is cleaner and there’s no hidden XML metadata. Plus, you get pixel-perfect typography.

Q: Can I use this with my existing Word resume? Yes—convert it to the YAML structure once. The upfront cost is 15 minutes; the payoff is every application after that takes seconds.

Q: Does Gemini ever hallucinate experience I don’t have? It can, if the prompt doesn’t constrain it tightly enough. The prompt in this guide explicitly says “do not fabricate numbers” and anchors the rewrite to the original bullets. If you see hallucinations, add a stronger constraint: “Only use information present in the original bullets.”

Q: How is this different from just pasting my resume into ChatGPT? This agent is repeatable, version-controlled, and produces a formatted PDF in one shot. It also integrates skill matching and batch processing, which a chat interface can’t do without manual steps. For engineers who ship, automation matters.

Q: What if I want to deploy this as a web app? The core logic is a single Python script. Wrap it in a FastAPI endpoint, add a file upload for the resume and JD, and you’ve got a SaaS. The Gemini free tier will limit you to ~60 users per minute, which is plenty for a personal tool or small team. If you’re thinking about shipping internal tools like this, the FDE Coach blog has more on the mindset and workflows that make these projects land with users.

Q: Can I use OpenRouter or another free LLM instead of Gemini? Absolutely. Swap the google-generativeai client for an OpenRouter call. The prompt structure stays the same. If you’re curious about free-tier model comparisons, our personal finance categorizer guide walks through using OpenRouter’s free models for structured extraction tasks.

#career#resume#automation#gemini#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