DEV Community

Shriram Shanbhag
Shriram Shanbhag

Posted on Edited on

How I Built ResumeTailor, A Local AI Resume Tailoring Agent

Applying for jobs is a numbers game, but sending a generic resume to 100 listings rarely works. Recruiter screens and applicant tracking systems look for specific alignments with the Job Description (JD).

Manually tailoring your resume for every single job can easily eat up hours of your day.

Screenshot of ResumeTailor AI Agent

To solve this, I built a local web application that automates this workflow:

  1. Scrapes the Job Description directly from a URL.
  2. Tailors your resume using a state-of-the-art LLM (with support for Google Gemini or NVIDIA APIs).
  3. Provides a side-by-side live editor to tweak the resume before saving it to your workspace.

Here's a link to the GitHub repo: resumeTailor

In this blog post, I'll walk you through how I built it using Python (FastAPI) and Vanilla Web Technologies (HTML, CSS, JS).


The Tech Stack

I kept the app lightweight, fast, and easy to run locally:

  • Backend: FastAPI (Python) for API endpoints, static file mounting, and interacting with Gemini/NVIDIA.
  • Frontend: HTML5, Vanilla CSS (SaaS Slate & Indigo Dark/Light theme), and vanilla JavaScript.
  • Markdown Parser: marked.js (loaded via CDN) to render live side-by-side previews.
  • LLM Integrations:
    • google-generativeai (Gemini API)
    • Direct requests connections to NVIDIA's OpenAI-compatible API.

Core Architecture & Code Walkthrough

Here’s a look at how the data flows from job posting to finished resume:

[Job URL] ➔ FastAPI (/api/extract-jd) ➔ Text Job Description
                                                │
[Base Resume] ➔ FastAPI (/api/tailor-resume) ◄──┘
                    │
            [Gemini or NVIDIA API]
                    │
                    ▼
          [Interactive Editor & Preview] ➔ Saved Markdown File
Enter fullscreen mode Exit fullscreen mode

1. Scraping the Job Description Text

Writing parser rules for every job board (LinkedIn, Indeed, greenhouse.io) is a nightmare. Instead, we use BeautifulSoup to strip script tags and grab cleaned textual content:

# app/main.py (excerpt)
@app.post("/api/extract-jd")
def extract_jd(req: ExtractRequest):
    try:
        headers = {"User-Agent": "Mozilla/5.0 ..."}
        res = requests.get(req.url, headers=headers, timeout=15)
        res.raise_for_status()

        soup = BeautifulSoup(res.text, "html.parser")

        # Strip script, style, nav, and header/footer elements
        for element in soup(["script", "style", "nav", "footer", "header"]):
            element.decompose()

        # Get cleaned plain text
        text = soup.get_text(separator="\n")
        cleaned_text = "\n".join(
            chunk.strip() for chunk in text.splitlines() if chunk.strip()
        )
        return {"text": cleaned_text}
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))
Enter fullscreen mode Exit fullscreen mode

2. The AI Tailoring Prompt

The prompt instructs the LLM to align skills and frame experiences honestly without inventing credentials:

prompt = f"""
You are an expert technical recruiter and resume writer. Your job is to tailor the candidate's resume to the provided Job Description (JD). 

Ensure that you:
1. Align the resume keywords, skills, and experience with the requirements highlighted in the JD.
2. Maintain honesty: Do not invent credentials, work experience, or certifications that are not present in the original resume.
3. Keep the output in standard Markdown format.

Job Description:
{req.job_description}

Original Resume:
{req.base_resume}
"""
Enter fullscreen mode Exit fullscreen mode

3. Supporting Gemini & NVIDIA APIs

To give users options, the backend handles both Google Gemini and NVIDIA API keys. Since NVIDIA uses an OpenAI-compatible spec, we connect to it using a standard POST request:

if req.provider == "nvidia":
    url = "https://integrate.api.nvidia.com/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {nvidia_key}",
        "Content-Type": "application/json"
    }
    body = {
        "model": req.nvidia_model or "meta/llama-3.1-70b-instruct",
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.2,
        "max_tokens": 4096
    }
    res = requests.post(url, json=body, headers=headers)
    text = res.json()["choices"][0]["message"]["content"]
Enter fullscreen mode Exit fullscreen mode

Building the Interactive UI Workspace

A backend API is only as good as the workspace that exposes it. We built a beautiful, side-by-side dashboard using CSS Grid and structured it into two panels:

  • Left Panel (Inputs): Textareas for your base resume, job description extraction URL, and optional tailoring constraints (e.g. \"Focus on my cloud DevOps skills\").
  • Right Panel (Interactive Workspace): A live editor and renderer area to inspect, iterate, and save your tailored resume.

1. Side-by-Side Dual Pane Layout

To make comparison natural, the application splits the viewport into a split-screen container:

.app-container {
    flex: 1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem;
    padding: 1.25rem;
}
Enter fullscreen mode Exit fullscreen mode

2. Live Markdown Parser & Edit Modes

Candidates want to see a formatted preview of their resume, but they also need the ability to make manual adjustments. We built a toggleable workspace using marked.js (a fast client-side markdown compiler):

// Switching between live markdown preview and raw text editing
btnToggleView.addEventListener('click', () => {
    if (currentMode === 'preview') {
        currentMode = 'edit';
        btnToggleView.textContent = 'Preview Mode';
        resumePreview.classList.add('hidden');
        resumeEditContainer.classList.remove('hidden');
    } else {
        currentMode = 'preview';
        btnToggleView.textContent = 'Edit Mode';
        tailoredContent = resumeEditor.value;
        renderPreview(tailoredContent);
        resumeEditContainer.classList.add('hidden');
        resumePreview.classList.remove('hidden');
    }
});

function renderPreview(markdownText) {
    resumePreview.innerHTML = marked.parse(markdownText);
}
Enter fullscreen mode Exit fullscreen mode

3. Local Storage Persistence

We store the base resume, API key preferences, and theme selections in localStorage. You can refresh the app or close your tab, and your environment remains exactly as you left it.


🚀 How to Run It Yourself

You can clone the code and start the project in under 2 minutes:

# Clone the repository
git clone https://github.com/ShriramShanbhag/resumeTailor
cd resumeTailor

# Setup virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Start local server
python -m uvicorn app.main:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8000, enter your API key in settings, and start tailoring!


💡 Key Takeaways

  • Scraping is simplified: Stripping script tags inside BeautifulSoup is a robust fallback for raw text extraction.
  • Client-side markdown rendering: Incorporating marked.js lets you write clean markdown directly into text fields and render it in real-time.
  • Root-level themes: Toggling dark/light modes on the <html> element instead of the <body> element prevents jarring layout flashes on page reload.

What integrations would you add next? Let me know in the comments below!

Top comments (0)