How the Hiring Agent Uses LLMs for Resume Section Parsing: A Technical Deep Dive
The Hiring Agent converts raw PDF resumes into structured JSON by delegating each section to a Large Language Model through the PDFHandler class, using Jinja templates for prompts and Pydantic models for type safety.
The interviewstreet/hiring-agent repository implements a modular parsing pipeline that treats LLMs as specialized extraction engines. Rather than submitting an entire resume in a single prompt, the system isolates each section—basics, work, education, skills, projects, and awards—and processes them independently through targeted LLM calls. This approach maximizes accuracy by allowing the model to focus on one semantic domain at a time.
Architecture Overview
The parsing workflow centers on the PDFHandler class in main/pdf.py, which orchestrates text extraction, prompt rendering, and result aggregation. When extract_json_from_pdf is invoked, the handler initializes an LLM provider, iterates over the six target sections, and constructs a fully typed JSONResume object.
The pipeline follows a strict sequence:
- Provider Initialization – Selects Ollama or Gemini based on environment configuration
- Template Rendering – Loads section-specific Jinja templates from
main/prompts/templates/ - Structured LLM Calls – Sends paired system and user messages for each section
- JSON Extraction – Cleans markdown fences and validates output
- Data Transformation – Converts raw dictionaries into Pydantic models via
transform.py
Provider Selection and Initialization
Before processing begins, the PDFHandler instantiates an LLM provider through initialize_llm_provider in main/llm_utils.py. The selection logic depends on the DEFAULT_MODEL constant defined in main/prompt.py and the presence of a GEMINI_API_KEY environment variable.
Gemini takes precedence when the configured model belongs to the Gemini family and a valid API key exists. Otherwise, the system falls back to an Ollama local instance. This dual-provider architecture ensures flexibility between cloud-based and on-premises deployments without changing the downstream parsing logic.
Prompt Engineering with Jinja Templates
Resume section parsing relies on precision-crafted prompts stored as Jinja templates. The TemplateManager class loads two types of templates for each section:
- User prompts (e.g.,
basics.jinja,work.jinja) containing the raw resume text and structured instructions - System messages from
system_message.jinjadefining the LLM's role as a strict JSON extractor
When _call_llm_for_section executes, it injects the extracted PDF text into the template and assembles a chat payload:
chat_params = {
"model": DEFAULT_MODEL,
"messages": [
{"role": "system", "content": section_system_message},
{"role": "user", "content": prompt},
],
"options": {"stream": False, "temperature": ..., "top_p": ...},
}
response = self.provider.chat(**chat_params, **kwargs)
This separation of concerns—system instructions versus user data—ensures consistent JSON formatting across different resume layouts.
The LLM Call Chain
The _extract_all_sections_separately method drives the iteration over the six resume sections. For each section, it delegates to _call_llm_for_section, which handles the actual API communication.
The provider abstraction allows the same code path to work for both Ollama and Gemini endpoints. Raw responses frequently contain markdown code fences, which extract_json_from_response in main/llm_utils.py strips before parsing.
JSON Processing and Transformation
After cleaning the LLM output, the handler parses the JSON string into a Python dictionary. This raw data then passes through transform_parsed_data in main/transform.py, which normalizes field names, handles nullable values, and maps the structure to the Pydantic schemas defined in main/models.py.
The final output is a JSONResume instance containing typed sub-models like Basics, Work, and Skills, ready for downstream evaluation and scoring algorithms.
Practical Implementation
Here is a complete example of parsing a resume using the Hiring Agent's public API:
from pdf import PDFHandler
# Path to a candidate's PDF resume
pdf_path = "candidates/jane_doe_resume.pdf"
handler = PDFHandler()
# Convert the PDF to a structured JSONResume object
json_resume = handler.extract_json_from_pdf(pdf_path)
if json_resume:
# Access structured data
print("Name:", json_resume.basics.name)
print("Work experiences:", len(json_resume.work or []))
print("Top skills:", [s.name for s in (json_resume.skills or [])[:5]])
else:
print("Failed to parse the resume.")
The extract_json_from_pdf method abstracts the entire pipeline: text extraction, per-section LLM calls, JSON cleaning, and model conversion.
Summary
- Modular Section Parsing: The Hiring Agent processes each resume section (basics, work, education, skills, projects, awards) as an isolated LLM call rather than submitting the entire document at once.
- Provider Flexibility: The
initialize_llm_providerfunction inmain/llm_utils.pyautomatically selects between Gemini (cloud) and Ollama (local) based on API key availability and model configuration. - Template-Driven Prompts: Jinja templates in
main/prompts/templates/enforce consistent JSON output formatting through separated system instructions and user content. - Type-Safe Output: Raw LLM responses undergo cleaning via
extract_json_from_responseand transformation throughtransform_parsed_datato produce validated Pydantic models defined inmain/models.py.
Frequently Asked Questions
How does the Hiring Agent handle different resume formats?
The system extracts raw text from PDFs using standard text extraction libraries, then relies on the LLM's semantic understanding to identify section boundaries. Because each section has its own specialized prompt template (e.g., work.jinja for employment history), the model can parse varied layouts without rigid regex patterns, making the parser resilient to formatting differences.
What happens if the LLM returns malformed JSON?
The extract_json_from_response function in main/llm_utils.py strips markdown fences and other extraneous formatting before parsing. If the resulting string still fails to parse as valid JSON, the specific section returns None and the aggregate resume object marks that section as missing, allowing the pipeline to continue processing other sections rather than failing entirely.
Can I use a different LLM provider than Gemini or Ollama?
While the current implementation in main/llm_utils.py only supports Gemini and Ollama, the provider architecture is modular. You would need to implement a new provider class following the same interface as GeminiProvider or OllamaProvider, then update initialize_llm_provider to instantiate your custom class based on configuration flags in main/prompt.py.
Why parse resume sections separately instead of in a single LLM call?
Processing sections separately through _extract_all_sections_separately reduces context window pressure and allows the model to focus on one semantic domain at a time. This approach also enables granular error handling—if the education section fails to parse, the work experience and skills sections remain usable, whereas a single large prompt would risk losing all data if the response format drifts.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →