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 TemplateManager class offers a uniform render_template() method used by pdf.py, github.py, and evaluator.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:

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 →