How to Create Custom Jinja Templates for Prompt Engineering in the Hiring Agent
Create a Jinja file in prompts/templates/, register it in TemplateManager._load_templates, and call render_template() to inject dynamic data into LLM prompts.
The interviewstreet/hiring-agent repository uses Jinja2 to manage LLM prompts for resume parsing tasks. By creating custom Jinja templates for prompt engineering, you can define new extraction sections without modifying the core Python logic. The system loads templates from disk, caches them in a Jinja Environment, and renders them with variables like {{ text_content }} at runtime.
Architecture of the Template System
The Hiring Agent separates prompt content from execution logic through a centralized template manager. Understanding this architecture is essential before extending it with custom templates.
Core Components
-
TemplateManager (
prompts/template_manager.py): Scans theprompts/templatesdirectory, initializes a JinjaEnvironmentwithFileSystemLoader, and caches templates in memory. It exposesrender_template(section_name, **kwargs)to generate final prompt strings. -
Template Files: Plain Jinja files (e.g.,
basics.jinja,work.jinja) stored inprompts/templates/. These files define system messages or user prompts using placeholders like{{ text_content }}. -
PDFHandler (
pdf.py): Consumes rendered templates by callingTemplateManager.render_template, then passes the result to the LLM provider via_call_llm_for_section.
The rendering flow follows four stages:
- Load:
TemplateManager.__init__creates the Jinja Environment pointing toprompts/templates. - Cache:
_load_templatesiterates over thetemplate_filesdictionary and stores eachTemplateobject inself._templates. - Render:
render_template(section, **kwargs)retrieves the cached template, injects variables, and returns the prompt string. - Consume: Callers like
PDFHandlerrequest specific sections and forward rendered prompts to the LLM.
Step-by-Step Guide to Creating Custom Jinja Templates
Step 1 – Create the Template File
Add a new Jinja file to prompts/templates/. The filename must match the entry you will register in the next step.
You are a senior hiring engineer. Extract the following fields from the provided resume markdown:
- leadership experience
- patents (if any)
--- Resume starts ---
{{ text_content }}
--- Resume ends ---
Return ONLY a JSON object:
{
"custom": {
"leadership": {{ ""|tojson }},
"patents": {{ ""|tojson }}
}
}
The {{ text_content }} placeholder receives the raw resume markdown. You can define additional variables such as {{ candidate_name }} or {{ job_role }} and pass them during rendering.
Step 2 – Register the Template
Edit TemplateManager._load_templates in prompts/template_manager.py to include your new file in the template_files dictionary:
template_files = {
# … existing entries …
"custom_section": "custom_section.jinja",
}
The dictionary key ("custom_section") becomes the identifier used when calling render_template.
Step 3 – Render from Your Code
Import TemplateManager and invoke render_template with your section key and required variables:
from prompts.template_manager import TemplateManager
tm = TemplateManager()
prompt = tm.render_template(
"custom_section",
text_content=resume_markdown,
candidate_name="Alice" # optional custom variable
)
The method returns a fully rendered string ready for the LLM.
Step 4 – Send to LLM
Follow the pattern in PDFHandler._call_llm_for_section to send the rendered prompt:
chat_params = {
"model": DEFAULT_MODEL,
"messages": [
{"role": "system", "content": prompt},
],
"options": {"temperature": 0.1, "top_p": 0.9},
}
response = provider.chat(**chat_params)
Parse the JSON response using the existing extract_json_from_response utility and map it to a Pydantic model (e.g., CustomSectionModel).
Complete Working Example
Here is a full implementation of a custom template for extracting leadership experience:
File: prompts/templates/leadership.jinja
You are an AI recruiter specializing in engineering leadership. Analyze the resume below and extract:
1. Team sizes managed
2. Duration in leadership roles
3. Notable achievements
Resume Content:
{{ text_content }}
Return ONLY valid JSON matching this structure:
{
"leadership": {
"team_sizes": [],
"durations": [],
"achievements": []
}
}
Registration: Update prompts/template_manager.py
class TemplateManager:
def _load_templates(self):
template_files = {
"basics": "basics.jinja",
"work": "work.jinja",
"leadership": "leadership.jinja", # new entry
}
# ... existing loading logic
Usage: Extend PDFHandler or create a standalone caller
def extract_leadership(self, resume_text: str) -> Optional[Dict]:
prompt = self.template_manager.render_template(
"leadership", text_content=resume_text
)
if not prompt:
logger.error("Failed to render leadership template")
return None
return self._call_llm_for_section(
"leadership", resume_text, prompt, LeadershipModel
)
Summary
- The TemplateManager in
prompts/template_manager.pyorchestrates Jinja template loading and rendering for all LLM interactions. - Create new templates as
.jinjafiles inprompts/templates/using standard Jinja2 syntax and placeholders like{{ text_content }}. - Register each template in the
template_filesdictionary within_load_templatesto enable caching and retrieval. - Call
render_template(section_name, **kwargs)to inject dynamic data and generate prompts for the LLM provider. - Reference
PDFHandler._call_llm_for_sectioninpdf.pyfor the standard pattern of sending rendered prompts and parsing JSON responses.
Frequently Asked Questions
How does the TemplateManager cache templates for performance?
During initialization, TemplateManager.__init__ invokes _load_templates, which iterates over the template_files dictionary and pre-compiles each Jinja file into a Template object stored in self._templates. This eliminates file I/O on subsequent calls to render_template, reducing latency when processing multiple resumes.
Can I pass custom variables beyond text_content to a template?
Yes. The render_template method accepts arbitrary keyword arguments via **kwargs. Define placeholders in your Jinja file (e.g., {{ candidate_name }} or {{ job_id }}), then pass them when rendering: tm.render_template("custom_section", text_content=resume, candidate_name="Bob"). All kwargs are injected into the template context automatically.
Where should I place new template files in the repository?
Place all custom Jinja templates in the prompts/templates/ directory at the repository root. The TemplateManager initializes its Jinja Environment with FileSystemLoader("prompts/templates"), so files outside this path will not be discovered or loaded during the caching phase.
How do I validate the JSON output from custom templates?
Follow the existing pattern in pdf.py where extract_json_from_response parses the LLM's string output into a Python dictionary. Define a Pydantic model (e.g., CustomSectionModel) that matches your template's JSON schema, then validate the parsed data against this model before using it in your application logic.
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 →