# How Hiring-Agent Structures and Manages LLM Prompts: The Jinja Template Architecture

> Discover how Hiring-Agent structures and manages LLM prompts using Jinja templates and a TemplateManager for efficient prompt rendering and maintainable code.

- Repository: [HackerRank/hiring-agent](https://github.com/interviewstreet/hiring-agent)
- Tags: architecture
- Published: 2026-06-30

---

**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`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) class defined in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/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:

```python

# 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/`](https://github.com/interviewstreet/hiring-agent/tree/main/prompts/templates). The manager maintains an explicit mapping of logical section names to physical filenames, covering resume extraction sections and system prompts:

```python
    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:

```python
    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`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/templates/basics.jinja) uses the `text_content` variable to inject resume markdown:

```jinja
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)](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:

```python

# 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)](https://github.com/interviewstreet/hiring-agent/blob/main/github.py) leverages the same manager for repository analysis prompts:

```python

# 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)](https://github.com/interviewstreet/hiring-agent/blob/main/evaluator.py) composes multi-part prompts by rendering both criteria and system message templates:

```python

# 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)](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`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), [`github.py`](https://github.com/interviewstreet/hiring-agent/blob/main/github.py), and [`evaluator.py`](https://github.com/interviewstreet/hiring-agent/blob/main/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`](https://github.com/interviewstreet/hiring-agent/blob/main/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)](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`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), [`evaluator.py`](https://github.com/interviewstreet/hiring-agent/blob/main/evaluator.py), or other modules.