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

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 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. 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 (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 (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) 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 (lines 65-104). These validated section objects are then assembled into a root JSONResume object at lines 261-279 of 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 validationextract_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:

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 to convert visual PDF layouts into structured markdown text, preserving document hierarchy during extraction.
  • Template-Driven Prompts – The TemplateManager in 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 (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 leverages the to_markdown utility from 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. 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →