How Hiring-Agent Structures and Manages LLM Prompts: The Jinja Template Architecture
Hiring-Agent centralizes all LLM prompts as Jinja2 templates in prompts/templates/ and uses a TemplateManager class to load, cache, and render them with runtime variables, creating a maintainable separation between prompt content and application logic.
The interviewstreet/hiring-agent repository orchestrates complex resume parsing and candidate evaluation through structured interactions with large language models. Understanding how prompts are structured and managed within hiring-agent reveals a template-driven architecture that treats prompt engineering as declarative, version-controlled assets while maintaining programmatic flexibility through Python's Jinja2 engine.
Core Prompt Architecture
The system revolves around a strict separation between prompt templates (static Jinja files) and prompt rendering (dynamic Python execution).
The TemplateManager Class
At the heart of the architecture lies the TemplateManager class defined in prompts/template_manager.py. This singleton-style manager initializes a Jinja2 Environment configured with FileSystemLoader targeting the prompts/templates/ directory, enabling automatic template discovery and in-memory caching.
The constructor sets up the rendering environment with whitespace control:
# prompts/template_manager.py
from jinja2 import Environment, FileSystemLoader
import os
from typing import Dict
class TemplateManager:
def __init__(self, template_dir: str = "prompts/templates"):
self.template_dir = template_dir
self.env = Environment(
loader=FileSystemLoader(template_dir),
trim_blocks=True,
lstrip_blocks=True
)
self._templates: Dict[str, Template] = {}
self._load_templates()
Template Storage and Logical Mapping
Raw templates reside as .jinja files in prompts/templates/. The manager maintains an explicit mapping of logical section names to physical filenames, covering resume extraction sections and system prompts:
def _load_templates(self):
template_files = {
"basics": "basics.jinja",
"work": "work.jinja",
"education": "education.jinja",
"skills": "skills.jinja",
"projects": "projects.jinja",
"awards": "awards.jinja",
"system_message": "system_message.jinja",
"github_project_selection": "github_project_selection.jinja",
"resume_evaluation_criteria": "resume_evaluation_criteria.jinja",
"resume_evaluation_system_message": "resume_evaluation_system_message.jinja",
}
for section_name, filename in template_files.items():
template_path = os.path.join(self.template_dir, filename)
if os.path.exists(template_path):
self._templates[section_name] = self.env.get_template(filename)
This mapping allows components to request prompts by semantic names (e.g., "work") while the manager handles filesystem resolution.
Rendering Prompts at Runtime
The render_template method provides the primary interface for converting stored templates into LLM-ready strings. It accepts a section identifier and keyword arguments that populate Jinja placeholders:
def render_template(self, section_name: str, **kwargs) -> str:
if section_name not in self._templates:
raise ValueError(f"Template {section_name} not found")
return self._templates[section_name].render(**kwargs)
A typical template like basics.jinja uses the text_content variable to inject resume markdown:
Extract ONLY the basic information (name, email, phone, location, profiles) from this resume.
--- The input resume markdown starts here ---
{{ text_content }}
--- The input resume markdown ends here ---
When rendered, the {{ text_content }} placeholder receives the actual resume text at runtime.
Integration Across the Codebase
The TemplateManager propagates through three primary integration points, ensuring consistent prompt generation across different evaluation stages.
PDF Resume Extraction
In [pdf.py](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), the processor instantiates TemplateManager to generate section-specific extraction prompts. For each resume section, it calls:
# pdf.py (excerpt)
from prompts.template_manager import TemplateManager
class PDFProcessor:
def __init__(self):
self.template_manager = TemplateManager()
def extract_work_history(self, resume_text: str):
prompt = self.template_manager.render_template("work", text_content=resume_text)
# prompt sent to LLM for structured extraction
GitHub Project Selection
The GitHub integration in [github.py](https://github.com/interviewstreet/hiring-agent/blob/main/github.py) leverages the same manager for repository analysis prompts:
# github.py (excerpt)
prompt = self.template_manager.render_template(
"github_project_selection",
repositories=repo_list,
candidate_name=name
)
Resume Evaluation Pipeline
The evaluator in [evaluator.py](https://github.com/interviewstreet/hiring-agent/blob/main/evaluator.py) composes multi-part prompts by rendering both criteria and system message templates:
# evaluator.py (excerpt)
criteria = self.template_manager.render_template(
"resume_evaluation_criteria",
text_content=resume_text
)
system_msg = self.template_manager.render_template(
"resume_evaluation_system_message"
)
# Combined and sent to LLM for scoring
While [prompt.py](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py) handles model selection and provider configuration, it receives the fully-rendered strings from TemplateManager rather than defining the prompt content itself.
Summary
Hiring-Agent's prompt management architecture provides several key advantages:
- Centralized Template Storage: All prompts live as Jinja files in
prompts/templates/, enabling version control and non-developer editing. - Single Rendering Interface: The
TemplateManagerclass offers a uniformrender_template()method used bypdf.py,github.py, andevaluator.py. - Runtime Variable Injection: Templates receive dynamic content (resume text, repository lists) through Jinja's
{{ variable }}syntax at execution time. - Decoupled Architecture: Business logic remains free of prompt text, allowing rapid iteration on LLM instructions without code changes.
Frequently Asked Questions
What templating engine does hiring-agent use for prompt management?
Hiring-Agent uses Jinja2, a Python-native templating engine. The TemplateManager class initializes jinja2.Environment with FileSystemLoader to load templates from the prompts/templates/ directory, utilizing options like trim_blocks=True to control whitespace in rendered output.
How do I add a new prompt template to the hiring-agent system?
Create a new .jinja file in prompts/templates/, then add the logical name and filename to the template_files dictionary in prompts/template_manager.py's _load_templates() method. Components can then call render_template("your_new_name", **context) to utilize the template.
Where does the actual LLM configuration reside if prompts are separate?
Model selection, temperature settings, and provider-specific configurations reside in [prompt.py](https://github.com/interviewstreet/hiring-agent/blob/main/prompt.py). This file handles the interface to language models but receives the actual prompt content as pre-rendered strings from TemplateManager, maintaining a clean separation between configuration and content.
Can I modify existing prompts without changing Python code?
Yes. Since prompts are stored as .jinja template files, you can edit the text in prompts/templates/basics.jinja or any other template file, and the changes will take effect immediately on the next application restart without modifying any Python source code in pdf.py, evaluator.py, or other modules.
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 →