# How LLM-Based Section Parsing Extracts Structured JSON from Resume PDFs

> Discover how our LLM-based section parsing transforms resume PDFs into structured JSON. Learn to extract valuable data efficiently for your hiring needs.

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

---

**The repository processes PDF résumés by extracting raw Markdown, prompting a Large Language Model (LLM) with section-specific templates, cleaning the JSON response, and mapping the results onto Pydantic models to produce a fully-typed `JSONResume` object.**

The `interviewstreet/hiring-agent` repository implements a sophisticated pipeline that converts unstructured PDF documents into structured JSON using LLM-based section parsing. This approach divides the résumé into logical segments—such as *basics*, *work*, and *education*—and processes each independently through a type-safe extraction workflow. By combining PyMuPDF for text extraction, Jinja templating for prompt engineering, and Pydantic for data validation, the system reliably transforms human-readable documents into machine-readable schemas.

## The Seven-Step Extraction Pipeline

The core logic resides in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), which orchestrates a seven-stage pipeline from raw PDF bytes to validated JSON objects.

### Step 1: PDF to Markdown Conversion

The process begins with `PDFHandler.extract_text_from_pdf`, which uses **PyMuPDF** to open the document and iterate over all pages. Each page converts to Markdown format via `to_markdown`, preserving structural hints like headings and bullet points that guide later section detection.

```python

# Located in pdf.py:52-58

def extract_text_from_pdf(self, pdf_path: str) -> str:
    doc = fitz.open(pdf_path)
    text = ""
    for page in doc:
        text += page.to_markdown()
    return text

```

### Step 2: Template-Based Prompt Construction

For each résumé section, the `TemplateManager.render_template` method loads a Jinja template containing a system message and user-side prompt. The template injects the extracted Markdown text and instructions specific to the target section (e.g., "You are extracting the *work* section").

```python

# Located in pdf.py:79-81

messages = self.template_manager.render_template(
    section_name=section,
    resume_text=markdown_text
)

```

### Step 3: LLM Invocation

The prepared messages route through `initialize_llm_provider` (defined in [`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)), which selects the appropriate API client based on configuration. The provider's `chat` method receives the messages along with model-specific parameters, returning a raw text response.

```python

# Located in pdf.py:88-99

provider = initialize_llm_provider()
response = provider.chat(
    messages=messages,
    model=DEFAULT_MODEL,
    temperature=0.1
)

```

### Step 4: JSON Extraction and Cleaning

Raw LLM outputs often contain explanatory text or markdown fences. The `extract_json_from_response` helper (in [`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py)) strips extraneous content, while a fallback brace-search algorithm locates the first `{` and last `}` to isolate the JSON payload before `json.loads` parses it.

```python

# Located in pdf.py:110-117

json_str = extract_json_from_response(response.text)
if not json_str:
    start = response.text.find('{')
    end = response.text.rfind('}') + 1
    json_str = response.text[start:end]
parsed_data = json.loads(json_str)

```

### Step 5: Data Normalization

The parsed dictionary passes through `transform_parsed_data` (in [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py)) to normalize field names, flatten nested structures, and reconcile the LLM's output with the expected schema shape. This step ensures the data conforms to the Pydantic model definitions before instantiation.

```python

# Located in pdf.py:119

normalized_data = transform_parsed_data(section, parsed_data)

```

### Step 6: Pydantic Model Construction

After all sections process independently, the pipeline constructs individual Pydantic objects such as `Basics(**data)` or `Work(**data)`. These populate the final `JSONResume` container, providing type safety and autocompletion for downstream consumers.

```python

# Located in pdf.py:102-112 and pdf.py:121-127

sections = {}
for section, data in parsed_sections.items():
    model_class = SECTION_MODELS[section]
    sections[section] = model_class(**data)
return JSONResume(**sections)

```

### Step 7: Orchestration Across Sections

The `PDFHandler._extract_all_sections_separately` method coordinates the workflow, iterating over the six core sections defined in the JSON Resume schema. It merges individual section results into a unified object, handling failures gracefully to ensure partial extractions don't invalidate the entire document.

```python

# Located in pdf.py:71-74 and pdf.py:89-99

def _extract_all_sections_separately(self, markdown_text: str) -> JSONResume:
    sections = {}
    for section in SECTION_NAMES:
        result = self._extract_section(section, markdown_text)
        sections[section] = result
    return JSONResume(**sections)

```

## Key Implementation Strategies

### Prompt Engineering for Structured Output

Each section utilizes a dedicated system message that constrains the LLM to output-specific schemas. The templates in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) explicitly request JSON format and provide field descriptions, reducing hallucination and ensuring consistent key naming across different résumé styles.

### Schema-Guided Generation

When a `return_model` is supplied, the LLM provider receives the model's JSON schema via a `format` parameter (see `pdf.py:101-104`). This schema-guided approach instructs the model to emit conforming JSON structures, significantly reducing parsing errors and eliminating the need for complex regex-based extraction.

### Robust Post-Processing

The dual-layer extraction strategy—first using `extract_json_from_response` then falling back to brace-search—ensures reliable JSON isolation even when models deviate from instructions. This robustness handles edge cases where the LLM wraps the JSON in markdown code blocks or adds conversational context.

## Complete Implementation Example

The following end-to-end example demonstrates how to extract structured data from a résumé PDF using the pipeline:

```python
from pdf import PDFHandler

# Initialize the handler (loads templates & LLM provider)

handler = PDFHandler()

# Path to a résumé PDF on disk

pdf_path = "example_resume.pdf"

# Extract a fully-typed JSONResume object

json_resume = handler.extract_json_from_pdf(pdf_path)

if json_resume:
    # Access typed fields or serialize to JSON

    print(f"Candidate: {json_resume.basics.name}")
    print(json_resume.json(indent=2))
else:
    print("Failed to parse the résumé.")

```

This snippet instantiates `PDFHandler`, which configures the default LLM provider and template manager. The `extract_json_from_pdf` method executes the complete pipeline—text extraction, per-section LLM calls, JSON cleaning, and model construction—returning a `JSONResume` object with full type safety.

## Summary

- **PDF Processing**: The system uses PyMuPDF in `pdf.py:52-58` to convert PDF pages to Markdown, preserving document structure.
- **Section-Specific Prompts**: `TemplateManager.render_template` generates targeted prompts for each résumé section, improving extraction accuracy.
- **JSON Cleaning**: `extract_json_from_response` and brace-search logic in `pdf.py:110-117` isolate valid JSON from LLM noise.
- **Type Safety**: Pydantic models in [`models.py`](https://github.com/interviewstreet/hiring-agent/blob/main/models.py) enforce schema compliance, with [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py) normalizing data between the LLM output and model requirements.
- **Orchestration**: `PDFHandler._extract_all_sections_separately` coordinates the six-section pipeline, assembling results into a final `JSONResume` instance.

## Frequently Asked Questions

### What LLM providers does the hiring-agent support?

The repository uses `initialize_llm_provider` in [`llm_utils.py`](https://github.com/interviewstreet/hiring-agent/blob/main/llm_utils.py) to abstract the underlying API client, allowing configuration of different providers through environment variables or settings. The specific model defaults to `DEFAULT_MODEL` from [`prompt.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py), but the architecture supports any provider implementing the `chat` interface with messages and formatting parameters.

### How does the system handle malformed PDFs or unscannable text?

The `extract_text_from_pdf` method in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) relies on PyMuPDF's robust extraction capabilities. If text extraction fails or returns empty content, the subsequent section parsing attempts will return empty results for those sections, but the pipeline continues processing other sections. The `JSONResume` model construction handles optional fields gracefully, allowing partial résumé parsing rather than complete failure.

### Why split the résumé into sections rather than parsing the entire document at once?

Processing sections independently via `_extract_all_sections_separately` reduces context window pressure and allows specialized prompting for each data type. According to the source code in `pdf.py:71-74`, this approach enables the LLM to focus on specific schemas (e.g., work history vs. education) and facilitates parallel processing opportunities while maintaining schema compliance through section-specific Pydantic models.

### Can the parser handle non-standard résumé formats or creative layouts?

The combination of Markdown conversion and LLM-based section parsing accommodates varied layouts better than rigid template matching. The `transform_parsed_data` function in [`transform.py`](https://github.com/interviewstreet/hiring-agent/blob/main/transform.py) normalizes heterogeneous field names and structures, mapping unconventional layouts to the standard JSON Resume schema. However, extreme visual layouts with heavy graphics may lose semantic structure during the PDF-to-Markdown conversion step.