# What Does the PDFHandler Class Do in hiring-agent? PDF-to-JSON Resume Parsing Explained

> Discover how the PDFHandler class converts PDF resumes to JSONResume objects using PyMuPDF and LLM processing for efficient data extraction.

- Repository: [HackerRank/hiring-agent](https://github.com/interviewstreet/hiring-agent)
- Tags: internals
- Published: 2026-07-09

---

**The `PDFHandler` class in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) orchestrates the end-to-end conversion of PDF resumes into fully-typed `JSONResume` objects by extracting markdown-friendly text with PyMuPDF and processing each section through a Large Language Model (LLM) with Pydantic validation.**

The `PDFHandler` class is the central component of the [interviewstreet/hiring-agent](https://github.com/interviewstreet/hiring-agent) repository, designed to transform unstructured PDF documents into structured data. Located at line 38 in [[`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py)](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L38), this class handles the complete pipeline from raw document ingestion to validated JSON output, making it the primary interface for resume parsing operations.

## Core Architecture of the PDFHandler Class

The `PDFHandler` implements a three-layer architecture that isolates document processing, language model interaction, and data validation. This separation ensures that PDF handling, LLM prompting, and data transformation remain distinct, testable units.

### PDF Text Extraction Layer

The [`extract_text_from_pdf`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L47-L64) method handles document ingestion using **PyMuPDF** (via `pymupdf`). It opens the PDF file, iterates through each page, and converts the content to markdown format using the `to_markdown` helper from [[`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py)](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py). This preserves document structure—such as headers and lists—before LLM processing, ensuring that formatting cues remain available for section identification.

### LLM Orchestration Layer

The class delegates section-specific extraction to dedicated methods that build targeted prompts:

- **`extract_basics_section`**: Handles personal information (name, email, phone)
- **`extract_work_section`**: Parses employment history
- **`extract_education_section`**: Extracts academic credentials
- **`extract_skills_section`**: Identifies technical and soft skills
- **`extract_projects_section`**: Captures portfolio projects
- **`extract_awards_section`**: Extracts achievements and certifications

Each method utilizes the [`TemplateManager`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) to render Jinja templates for system messages and user prompts, then delegates to [`_call_llm_for_section`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L66-L134). This internal method:
1. Constructs the system message using the section-specific template
2. Builds the user prompt with the extracted PDF text
3. Invokes the LLM provider (initialized via [`_initialize_llm_provider`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L44-L46))
4. Extracts and validates JSON from the response
5. Applies `transform_parsed_data` to normalize the output

### Data Transformation Layer

The [`_extract_all_sections_separately`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L266-L324) method serves as the aggregation engine. It iterates through all required resume sections, dispatches extraction via [`_extract_section_data`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L219-L227), merges the results into a single dictionary, and instantiates the Pydantic models defined in [[`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py)](https://github.com/interviewstreet/hiring-agent/blob/main/models.py). This includes creating `Basics`, `Work`, `Education`, and other typed sub-objects before assembling the final `JSONResume` instance.

## Key Methods and Implementation Details

Understanding the `PDFHandler` requires familiarity with its primary interface methods and internal workflow:

**`__init__`** (lines 38-42): Initializes the handler by creating a `TemplateManager` instance and preparing the LLM provider based on the `DEFAULT_MODEL` setting from [[`prompt.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py)](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py).

**`extract_json_from_pdf`** (lines 99-118): The public API entry point that accepts a file path, orchestrates text extraction, and manages the section-by-section parsing pipeline. Returns a validated `JSONResume` object or `None` on failure.

**`_call_llm_for_section`** (lines 66-134): The core LLM interaction logic that:
- Accepts a section name and markdown text
- Loads appropriate templates from [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py)
- Invokes the provider with error handling
- Parses JSON responses using utilities from [[`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)

**`_extract_section_data`** (lines 219-227): A dispatcher that maps section names (e.g., `"basics"`, `"work"`) to their corresponding `extract_*_section` methods, enabling the loop-based aggregation in `_extract_all_sections_separately`.

## Practical Usage Example

Implementing the `PDFHandler` requires minimal boilerplate. The class automatically handles LLM provider selection and template loading:

```python
from pdf import PDFHandler

# Initialise the handler (templates & LLM provider are loaded automatically)

handler = PDFHandler()

# Path to a candidate's resume PDF

pdf_path = "candidates/jane_doe_resume.pdf"

# Convert the PDF → structured JSONResume (Pydantic model)

resume = handler.extract_json_from_pdf(pdf_path)

if resume:
    # `resume` is a JSONResume with typed sub‑objects (Basics, Work, etc.)

    print("Name:", resume.basics.name)
    print("Work experience count:", len(resume.work))
else:
    print("Failed to parse the PDF.")

```

**Key implementation details:**
- The handler automatically selects the default LLM model specified in [`prompt.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py)
- The returned `JSONResume` object provides IDE autocompletion and runtime validation through Pydantic
- Method returns `None` on parsing failures, allowing graceful error handling by the caller

## Integration with the Resume Processing Pipeline

The `PDFHandler` class does not operate in isolation. It relies on several supporting modules that constitute the hiring-agent's extraction pipeline:

| File | Role in PDFHandler Workflow |
|------|----------------------------|
| **[`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py)** | Provides Pydantic schemas (`JSONResume`, `Basics`, `Work`, `Education`, etc.) that validate and type the extracted data |
| **[`prompt.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py)** | Stores `DEFAULT_MODEL` configuration and API keys required by [`_initialize_llm_provider`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py#L44-L46) |
| **[`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py)** | Loads Jinja2 templates for system messages and section-specific user prompts used in `_call_llm_for_section` |
| **[`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py)** | Contains the `to_markdown` function that renders PDF pages to markdown before LLM processing |
| **[`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)** | Supplies provider initialization logic and JSON extraction utilities for cleaning LLM responses |

## Summary

- The **`PDFHandler`** class in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) serves as the primary orchestrator for converting PDF resumes into structured `JSONResume` objects within the interviewstreet/hiring-agent repository.
- It uses **PyMuPDF** via `extract_text_from_pdf` to convert PDF pages to markdown-friendly text while preserving structural elements.
- Section extraction occurs through **`_call_llm_for_section`**, which constructs targeted prompts, invokes the LLM, and validates JSON responses against Pydantic models.
- The **`extract_json_from_pdf`** method provides a single public interface that returns fully-typed resume data or `None` on failure.
- The class architecture separates concerns between document parsing (PyMuPDF), language model interaction (LLM providers), and data validation (Pydantic models).

## Frequently Asked Questions

### How does PDFHandler handle different PDF formats and layouts?

The `PDFHandler` utilizes PyMuPDF's `to_markdown` function from [`pymupdf_rag.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pymupdf_rag.py) to normalize diverse PDF layouts into markdown text before LLM processing. This conversion preserves hierarchical structure (headers, lists, paragraphs) as markdown syntax, allowing the LLM to interpret document organization regardless of original formatting. The method handles both text-based and scanned PDFs that PyMuPDF can parse, though extraction quality depends on the underlying PDF structure.

### What happens if the LLM returns malformed JSON or invalid data?

The `_call_llm_for_section` method (lines 66-134) includes validation logic that extracts JSON payloads from LLM responses and validates them against expected schemas. If parsing fails or data is invalid, the method returns `None` for that specific section. The calling code in `_extract_all_sections_separately` handles these failures gracefully, omitting invalid sections from the final `JSONResume` assembly rather than crashing the entire pipeline.

### Can I use PDFHandler with different LLM providers?

Yes, the `PDFHandler` supports provider swapping through the `_initialize_llm_provider` method (lines 44-46), which selects the appropriate implementation based on the `DEFAULT_MODEL` configuration in [`prompt.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py). The class uses abstraction layers from [`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py) to manage provider-specific initialization, allowing you to switch between OpenAI, Anthropic, or other supported models without modifying the core extraction logic in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py).

### What is the performance characteristic of the extract_json_from_pdf method?

The method calls `_extract_all_sections_separately` (lines 266-324), which processes each resume section sequentially through the LLM. Performance depends on the number of sections parsed (basics, work, education, skills, projects, awards) and the LLM's response time. The method includes runtime measurement logging and returns fully-materialized Pydantic objects, meaning all LLM calls complete before the function returns—there is no streaming or async processing in the current implementation.