# How the Interviewstreet Hiring Agent Extracts Data from PDFs for Resume Analysis

> Learn how the Interviewstreet hiring agent extracts data from PDFs for resume analysis. Discover its four-stage pipeline using PyMuPDF, Jinja2, and Pydantic for efficient JSON conversion.

- Repository: [HackerRank/hiring-agent](https://github.com/interviewstreet/hiring-agent)
- Tags: how-to-guide
- Published: 2026-07-19

---

**The Interviewstreet hiring-agent repository converts unstructured PDF résumés into validated JSON objects using a four-stage pipeline that leverages PyMuPDF for text extraction, Jinja2 templating for LLM prompts, and Pydantic models for strict type validation.**

The interviewstreet/hiring-agent repository solves the challenge of automated résumé parsing by implementing a robust extraction system that transforms visual PDF layouts into machine-readable structured data. This open-source hiring agent combines document processing libraries with large language models to extract candidate information with high accuracy and type safety. Understanding this extraction flow is essential for developers looking to integrate automated resume analysis into their hiring workflows.

## The Four-Stage PDF Extraction Pipeline

The hiring agent processes PDF résumés through a strictly defined pipeline that isolates document parsing, prompt engineering, LLM interaction, and data validation into discrete stages.

### Stage 1: PDF to Markdown Conversion with PyMuPDF

The extraction begins in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) where the `PDFHandler.extract_text_from_pdf` method opens the PDF using **PyMuPDF** (`fitz`). According to the source code at lines 47-61, the method walks every page of the document and converts the visual layout into plain-text markdown using the `to_markdown` utility found in [`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py). This produces a single string containing the complete résumé text while preserving structural hints from the original formatting.

### Stage 2: Jinja Template-Based Prompt Generation

For each résumé section—**basics**, **work**, **education**, **skills**, **projects**, and **awards**—the handler invokes the `TemplateManager` to render specialized prompts. Located in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) (lines 21-44), this component uses Jinja templates stored under `prompts/templates/*.jinja` to inject the raw markdown content (`text_content`) into structured LLM instructions. Each template contains system-message instructions that direct the model to output JSON matching specific schema requirements.

### Stage 3: LLM-Powered Structured Data Extraction

The rendered prompt is transmitted to the selected LLM provider—either Ollama or Gemini—via the `initialize_llm_provider` utility. As implemented in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) (lines 66-114), the provider's `chat` method receives the formatted payload including optional JSON schema constraints when a `return_model` is specified. The response undergoes cleaning via `extract_json_from_response` (defined in [`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)) to isolate the first valid JSON block, followed by normalization through `transform_parsed_data` to handle edge cases in LLM outputs.

### Stage 4: Pydantic Model Construction and Validation

In the final stage, parsed data for each section is instantiated into corresponding **Pydantic section models** including `BasicsSection`, `WorkSection`, `EducationSection`, and others defined in [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py) (lines 65-104). These validated section objects are then assembled into a root `JSONResume` object at lines 261-279 of [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py). This construction guarantees that downstream code receives a fully-typed, validated representation where fields like `resume.basics.name` or `resume.skills` adhere to strict type contracts, preventing runtime errors from malformed inputs.

## Deep Dive: The Complete Extraction Flow

The `PDFHandler` orchestrates a fault-tolerant workflow that prevents partial data corruption:

1. **File validation** – `extract_text_from_pdf` raises `FileNotFoundError` immediately if the path is invalid, preventing wasted computation on missing files.

2. **Document processing** – The system calls `pymupdf.open(pdf_path)` to create a document object, then executes `to_markdown(doc, pages=range(doc.page_count))` to extract text from all pages into a unified markdown string.

3. **Section-wise processing** – The private method `_extract_all_sections_separately` iterates over the fixed section list, delegating to `_extract_section_data` and specific `extract_*_section` methods for each résumé component.

4. **Schema-enforced generation** – When calling `_call_llm_for_section`, the system passes `DEFAULT_MODEL` and `MODEL_PARAMETERS` along with the `format` kwarg containing the Pydantic model's JSON schema, enabling LLMs that support structured outputs (like Gemini or Ollama) to constrain their responses to valid JSON.

5. **Error isolation** – Any failure during JSON parsing, normalization, or Pydantic validation causes the entire extraction to abort rather than returning partially-filled resumes, ensuring data integrity across the pipeline.

## Practical Code Example

The public API exposes a simple interface for extracting structured résumé data from PDF files:

```python
from pdf import PDFHandler

# Initialise the handler (loads templates and selects the LLM provider)

handler = PDFHandler()

# Path to a candidate's résumé PDF

pdf_path = "candidates/alice_smith_resume.pdf"

# Extract a typed JSONResume object

resume = handler.extract_json_from_pdf(pdf_path)

if resume:
    # Access structured fields safely with IDE autocomplete support

    print("Name:", resume.basics.name)
    print("Primary skills:", [s.name for s in resume.skills or []])
    print("Work history entries:", len(resume.work or []))
else:
    print("Failed to parse the résumé.")

```

This example demonstrates the `extract_json_from_pdf` entry point that the rest of the hiring agent uses to obtain fully-validated résumé objects ready for storage or analysis.

## Summary

- **PyMuPDF Integration** – The hiring agent uses `to_markdown` from [`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py) to convert visual PDF layouts into structured markdown text, preserving document hierarchy during extraction.
- **Template-Driven Prompts** – The `TemplateManager` in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) renders Jinja templates for each résumé section, ensuring consistent LLM instructions that request specific JSON output formats.
- **Type-Safe Validation** – All extracted data passes through Pydantic models defined in [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py) (including `JSONResume` and section-specific classes), guaranteeing that the final output conforms to expected schemas.
- **Fault Isolation** – The pipeline validates file existence early, normalizes LLM responses to extract JSON blocks, and aborts completely on any parsing error to prevent corrupt data propagation.

## Frequently Asked Questions

### What PDF library does the hiring agent use to extract data from PDFs for resume analysis?

The repository uses **PyMuPDF** (also known as `fitz`) as its primary PDF processing engine. Specifically, the `extract_text_from_pdf` method in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) leverages the `to_markdown` utility from [`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py) to convert PDF pages into markdown format, which preserves structural information like headers and lists better than raw text extraction alone.

### How does the system ensure extracted resume data matches the expected JSON schema?

Data integrity is enforced through **Pydantic models** defined in [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py). After the LLM returns a JSON response, the pipeline validates the parsed dictionary against section-specific models like `BasicsSection` and `WorkSection` before assembling them into the final `JSONResume` object. Additionally, when supported by the LLM provider (Gemini or Ollama), the system passes the Pydantic model's JSON schema via the `format` parameter to constrain the LLM's output structure at generation time.

### Can the extraction pipeline handle corrupted or scanned PDF files?

The pipeline includes defensive checks at multiple layers. `extract_text_from_pdf` raises `FileNotFoundError` for invalid paths before processing begins. During extraction, PyMuPDF handles various PDF encodings, though scanned image-based PDFs would require OCR preprocessing (not currently implemented in the base `to_markdown` flow). The LLM normalization step in `transform_parsed_data` attempts to repair minor JSON formatting errors in the model's response, but complete extraction failures result in `None` being returned rather than partial data.

### Is it possible to customize the prompts for specific resume formats or industries?

Yes, the **Jinja template system** in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) allows full customization of extraction prompts. Templates are stored in `prompts/templates/*.jinja` and are rendered dynamically for each section (basics, work, education, etc.). Users can modify these templates to include industry-specific terminology, alternate section names, or custom extraction instructions without changing the core Python logic in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py).