Template

User-defined prompt template engine for LLM applications

Classes

Class Description
PromptTemplate Prompt template with Jinja2 syntax for AI/LLM workflows.
TemplateEnvironment Shared configuration for a group of templates.
DebugResult Result of rendering with debug mode enabled.
DebugSpan Span in rendered template output for debug visualization.
ValidationResult Result of template input validation.

class PromptTemplate

PromptTemplate(
    self,
    source: str,
    strict: bool = True
)

Prompt template with Jinja2 syntax for AI/LLM workflows.

A PromptTemplate compiles once and can be rendered many times with different variables. Supports standard Jinja2 syntax including control flow, filters, and functions - enabling seamless migration from existing Jinja2 templates.

Features for Prompt Engineering

Introspection - Discover required variables without running:

>>> t = PromptTemplate("Hello {{ name }}, you are {{ age }} years old")
>>> t.input_variables
{'name', 'age'}

Partial Application - Pre-fill variables for pipelines:

>>> system = PromptTemplate("System: {{ persona }}\\nUser: {{ query }}")
>>> chat = system.partial(persona="You are a helpful assistant")
>>> chat(query="Hello!")  # Only need to provide query
'System: You are a helpful assistant\nUser: Hello!'

Custom Filters - Register Python functions as template filters:

>>> t = PromptTemplate("{{ name | shout }}")
>>> t.register_filter("shout", lambda s: s.upper() + "!!!")
>>> t(name="hello")
'HELLO!!!'
Chat Formatting

PromptTemplate includes presets for common chat formats:

>>> t = PromptTemplate.chatml()
>>> prompt = t.apply([
...     {"role": "user", "content": "Hello!"},
... ])

Available presets: chatml(), llama2(), alpaca(), vicuna(), zephyr()

Basic Usage
>>> from talu import PromptTemplate

# Create template >>> t = PromptTemplate("Hello {{ name }}!")

# Render with variables (three equivalent ways) >>> t(name="World") # Callable (recommended) 'Hello World!' >>> t.format(name="World") # Like str.format() 'Hello World!' >>> t.render(name="World") # Like Jinja2 'Hello World!'

# RAG example >>> rag = PromptTemplate(''' ... Context: ... {% for doc in docs %} ... - {{ doc.content }} ... {% endfor %} ... ... Question: {{ question }} ... ''') >>> rag(docs=[{"content": "Paris is in France."}], ... question="Where is Paris?")

Supported Jinja2 Features
  • Fully Supported:
  • Variables: {{ name }}, {{ user.name }}
  • Control flow: {% if %}, {% for %}, {% set %}, {% macro %}
  • Filters: | upper, | lower, | join, | default, etc.
  • Operators: +, -, *, /, in, not in, is, and, or
  • Functions: range(), dict(), namespace()
  • Comments: {# comment #}
  • Whitespace control: {{- name -}}, {%- if -%}
  • Custom Python filters via register_filter()
  • Template composition via {% include %} (see below)

Template Composition: The {% include %} tag allows including other templates dynamically. The argument is an expression that evaluates to a template string at runtime - it does not load from the filesystem:

>>> t = PromptTemplate("{% include header %}Body")
>>> t(header="=== {{ title }} ===", title="My Doc")
'=== My Doc ===Body'
This design differs from standard Jinja2 (which loads files) for
security reasons - templates cannot access the filesystem. Pass
template strings as variables instead.
Included templates have access to the parent template's context,
including macros defined in previously included templates:
>>> t = PromptTemplate("{% include utils %}{{ greet('World') }}")
>>> t(utils="{% macro greet(n) %}Hello {{ n }}!{% endmacro %}")
'Hello World!'
  • Not Supported:
  • Template inheritance ({% extends %}, {% block %}) - use {% include %} for composition instead
  • File-based includes - pass template strings as variables for security
Parameters
source

The template string with Jinja2 syntax.

Raises
TemplateSyntaxError

If the template has invalid syntax.

Quick Reference

Properties

Name Type
input_variables set[str]
source str
strict bool
supports_system_role bool
supports_tools bool

Methods

Method Description
__call__() Render the template with the given variables.
apply() Render chat messages using this template.
format() Render the template with the given variables.
from_chat_template() Load a model's chat template as an inspectable ...
from_file() Load a template from a file.
from_preset() Create a PromptTemplate from a built-in chat fo...
partial() Return a new PromptTemplate with some variables...
register_filter() Register a custom Python filter function.
render() Render the template with the given variables.
validate() Validate inputs and prepare for efficient rende...

Properties

input_variables: set[str]

Return the set of variable names required by this template.

This enables building tools on top of templates (CLIs, web UIs, validators) that need to know what inputs are expected without executing the template.

Returns

Set of variable names found in the template source.

Note

This uses AST-based extraction via the Zig template engine, which accurately detects all required variables including:

  • Simple variables: {{ name }}
  • Attribute access: {{ user.name }} (extracts user)
  • Array indexing: {{ items[0] }} (extracts items)
  • Dictionary access: {{ data['key'] }} (extracts data)
  • Complex expressions: {{ a if b else c }}
  • Nested in filters: {{ name | default(fallback) }}
  • In conditions: {% if show %}
  • In loops (the iterable): {% for x in items %}
  • Automatically excluded:
  • Loop iteration variables: x in {% for x in items %}
  • Set variables: x in {% set x = ... %}
  • Macro parameters
  • Built-ins: loop, true, false, none, range, etc.
Example
>>> t = PromptTemplate("Hello {{ name }}, you are {{ age }} years old")
>>> t.input_variables
{'name', 'age'}
>>> rag = PromptTemplate('''
... {% for doc in docs %}
... {{ doc.content }}
... {% endfor %}
... Question: {{ question }}
... ''')
>>> rag.input_variables
{'docs', 'question'}

source: str

The original template source string.

strict: bool

Whether strict mode is enabled (undefined variables raise errors).

supports_system_role: bool

Check if this chat template explicitly handles the 'system' role.

Returns True if the template source contains explicit handling for system messages (e.g., message.role == 'system' or role == "system").

  • This helps Chat implementations decide whether to:
  • Pass system messages directly (if supported)
  • Prepend system content to the first user message (if not supported)
Returns

True if template handles system role, False otherwise.

Example
>>> t = PromptTemplate.chatml()
>>> t.supports_system_role  # ChatML handles any role
True
>>> t = PromptTemplate("{{ messages[0].content }}")
>>> t.supports_system_role  # No role handling
False
Note

This is a heuristic based on source inspection. Templates that use {{ message.role }} directly (like ChatML) will return True because they handle any role generically.

supports_tools: bool

Check if this chat template has built-in tool/function calling support.

Returns True if the template source contains handling for tools, functions, or available_tools variables (common in HuggingFace templates for models like Llama-3, Qwen, etc.).

  • This helps Chat implementations decide whether to:
  • Let the template handle tool formatting (if supported)
  • Handle tool formatting separately (if not supported)

When a template has built-in tool support but the Chat module also handles tools, pass an empty tools list to the template to avoid conflicts.

Returns

True if template has tool handling, False otherwise.

Example
>>> t = PromptTemplate.chatml()
>>> t.supports_tools  # Basic ChatML has no tool handling
False
Note

This is a heuristic. Templates with tool support typically check for tools, available_tools, or functions variables.

Methods

def __call__(
    self,
    debug: bool = False,
    strict: bool | None = None,
    variables: Any
)str | DebugResult

Render the template with the given variables.

Parameters
debug

If True, return DebugResult with span tracking.

strict

Override strict mode for this render only. **variables: Variables to substitute in the template.

Returns

Rendered string, or DebugResult if debug=True.

Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t(name="World")
'Hello World!'

def apply(
    self,
    messages: list[dict[str, Any]],
    debug: bool = False,
    strict: bool | None = None,
    add_generation_prompt: bool = True,
    bos_token: str = '',
    eos_token: str = '',
    kwargs: Any
)str | DebugResult

Render chat messages using this template.

Convenience wrapper around __call__() with named parameters for common chat template variables.

Parameters
messages

List of message dicts with role and content keys.

debug

If True, return DebugResult with span tracking.

strict

Override strict mode for this render only.

add_generation_prompt

Add assistant marker at end (default True).

bos_token

Beginning of sequence token (model-specific).

eos_token

End of sequence token (model-specific). **kwargs: Additional template variables (e.g., tools).

Returns

Formatted prompt string, or DebugResult if debug=True.

Example
>>> t = PromptTemplate.chatml()
>>> prompt = t.apply([
...     {"role": "system", "content": "You are helpful."},
...     {"role": "user", "content": "Hello!"},
... ])

def format(self, variables: Any)str

Render the template with the given variables.

Familiar API for str.format() users.

Parameters

**variables: Variables to substitute in the template.

Returns

The rendered template string.

Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t.format(name="World")
'Hello World!'

def from_chat_template(
    cls,
    model: str,
    strict: bool = False
)PromptTemplate

Load a model's chat template as an inspectable PromptTemplate.

Parameters
model

Model path or HuggingFace ID (e.g., "Qwen/Qwen3-0.6B").

strict

If True, undefined variables raise errors.

Returns

PromptTemplate with the model's chat template.

Raises
FileNotFoundError

If model not found or has no chat template.

Example
>>> t = PromptTemplate.from_chat_template("Qwen/Qwen3-0.6B")
>>> t.input_variables
{'messages', 'add_generation_prompt', ...}

def from_file(cls, path: str)PromptTemplate

Load a template from a file.

Parameters
path

Path to the template file.

Returns

A new PromptTemplate instance.

Raises
FileNotFoundError

If the file doesn't exist.

TemplateSyntaxError

If template syntax is invalid.

Example
>>> template = PromptTemplate.from_file("prompts/rag.j2")
>>> result = template(documents=docs, question="...")

def from_preset(cls, name: str)PromptTemplate

Create a PromptTemplate from a built-in chat format preset.

Available presets: chatml, llama2, alpaca, vicuna, zephyr.

Parameters
name

Preset name (case-sensitive).

Returns

PromptTemplate configured for the specified format.

Raises
ValueError

If name is not a known preset.

Example
>>> t = PromptTemplate.from_preset("chatml")
>>> t.apply([{"role": "user", "content": "Hi"}])
'<|im_start|>user\\nHi<|im_end|>\\n<|im_start|>assistant\\n'

def partial(self, kwargs: Any)PromptTemplate

Return a new PromptTemplate with some variables pre-filled.

Parameters

**kwargs: Variables to pre-fill in the new template.

Returns

A new PromptTemplate with the variables baked in.

Example
>>> t = PromptTemplate("{{ persona }}\\n{{ query }}")
>>> chat = t.partial(persona="You are helpful")
>>> chat(query="Hello!")
'You are helpful\nHello!'

def register_filter(
    self,
    name: str,
    func: Any
)PromptTemplate

Register a custom Python filter function.

Parameters
name

Filter name to use in templates (e.g., {{ x | name }}).

func

Callable that takes the piped value and returns a result.

Returns

Self, for method chaining.

Example
>>> t = PromptTemplate("{{ name | shout }}")
>>> t.register_filter("shout", lambda s: s.upper() + "!")
>>> t(name="hello")
'HELLO!'

def render(
    self,
    strict: bool | None = None,
    variables: Any
)str

Render the template with the given variables.

Familiar API for Jinja2 users.

Parameters
strict

Override the instance's strict mode for this render only. If None (default), uses the instance's strict setting. **variables: Variables to substitute in the template.

Returns

The rendered template string.

Example
>>> t = PromptTemplate("Hello {{ name }}!")
>>> t.render(name="World")
'Hello World!'

def validate(self, kwargs: Any)ValidationResult

Validate inputs and prepare for efficient rendering.

Parameters

**kwargs: Variables to validate against template requirements.

Returns

ValidationResult with is_valid, required, optional, extra. Use result.render() to render without re-serializing variables.

Example
>>> t = PromptTemplate("Hello {{ name }}, age {{ age }}")
>>> result = t.validate(name="Alice")
>>> result.is_valid
False
>>> result.required
{'age'}

class TemplateEnvironment

TemplateEnvironment(self, strict: bool = True)

Shared configuration for a group of templates.

Use an environment when you have multiple templates that share:

This eliminates boilerplate when you have many templates that need the same configuration.

Parameters
strict

Default strict mode for templates. When True (default), undefined variables raise errors instead of rendering as empty. This prevents silent failures in LLM prompts.

Example

Set up an environment for your application:

>>> from talu import TemplateEnvironment
>>> env = TemplateEnvironment()  # strict=True by default
>>> env.globals["app_name"] = "MyAssistant"
>>> env.globals["version"] = "2.0"
>>> env.register_filter("currency", lambda x: f"${x:,.2f}")
Create templates from the environment::
>>> greeting = env.from_string("Welcome to {{ app_name }} v{{ version }}!")
>>> greeting()
'Welcome to MyAssistant v2.0!'
>>> invoice = env.from_string("Total: {{ amount | currency }}")
>>> invoice(amount=99.50)
'Total: $99.50'
Load templates from files::
>>> prompt = env.from_file("prompts/rag_context.j2")

See Also -------- PromptTemplate : For standalone templates without shared config.

Quick Reference

Properties

Name Type
filters dict[str, Callable]
globals dict[str, Any]
strict bool

Methods

Method Description
from_file() Load a template from a file with this environme...
from_string() Create a template from a string with this envir...
register_filter() Register a filter for this environment.

Properties

filters: dict[str, Callable]

Custom filters for this environment.

Modify directly to add/remove filters, or use register_filter().

Example
>>> env.filters["shout"] = lambda s: s.upper() + "!"

globals: dict[str, Any]

Global variables available to all templates in this environment.

These are merged with render-time variables, with render-time variables taking precedence.

Example
>>> env.globals["app_name"] = "MyApp"
>>> env.globals["debug"] = True

strict: bool

Default strict mode for templates created from this environment.

Methods

def from_file(
    self,
    path: str | Path,
    strict: bool | None = None
)PromptTemplate

Load a template from a file with this environment's configuration.

Parameters
path

Path to template file.

strict

Override the environment's default strict mode.

Returns

A PromptTemplate with this environment's filters and globals.

Raises
FileNotFoundError

If the template file doesn't exist.

Example
>>> t = env.from_file("prompts/greeting.j2")

def from_string(
    self,
    source: str,
    strict: bool | None = None
)PromptTemplate

Create a template from a string with this environment's configuration.

Parameters
source

Template source string (Jinja2 syntax).

strict

Override the environment's default strict mode.

Returns

A PromptTemplate with this environment's filters and globals.

Example
>>> t = env.from_string("Hello {{ name | upper }}!")
>>> t(name="world")
'Hello WORLD!'

def register_filter(
    self,
    name: str,
    func: Callable
)TemplateEnvironment

Register a filter for this environment.

Parameters
name

Filter name to use in templates.

func

Filter function.

Returns

Self, for method chaining.

Raises
ValidationError

If func is not callable.

Example
>>> env.register_filter("upper", str.upper).register_filter("trim", str.strip)

class DebugResult

DebugResult(
    self,
    output: str,
    spans: list[DebugSpan]
)

Result of rendering with debug mode enabled.

Contains the rendered output plus detailed span information showing which parts came from variables vs static text.

Attributes
output

The rendered template string.

spans

List of DebugSpan showing the source of each part.

Quick Reference

Methods

Method Description
format_ansi() Format output with ANSI colors for terminal dis...
format_plain() Format output with plain text markers (no ANSI).

Methods

def format_ansi(self)str

Format output with ANSI colors for terminal display.

  • Static text: normal (no color)
  • Variables: cyan with variable name in parentheses
  • Expressions: yellow
Note

Empty variable values produce no output, so they don't appear in the span list. Use PromptTemplate.validate() before rendering to detect empty or missing values.

Returns

Formatted string with ANSI escape codes.

def format_plain(self)str

Format output with plain text markers (no ANSI).

  • Static text: unchanged
  • Variables: «value» (var_name)
  • Expressions: «value»
Note

Empty variable values produce no output, so they don't appear in the span list. Use PromptTemplate.validate() before rendering to detect empty or missing values.

Returns

Formatted string with plain text markers.


class DebugSpan

DebugSpan(
    self,
    start: int,
    end: int,
    source: str,
    text: str
)

Span in rendered template output for debug visualization.

Tracks which parts of the output came from variables vs static template text. This enables debugging "why did the model hallucinate?" by showing exactly which variable produced which part of the output.

Attributes
start

Start position in output (inclusive).

end

End position in output (exclusive).

source

What produced this span - "static", variable path (e.g., "name", "user.email"), or "expression".

text

The actual text content of this span.

Quick Reference

Properties

Name Type
is_expression bool
is_static bool
is_variable bool

Properties

is_expression: bool

Whether this span came from a complex expression.

is_static: bool

Whether this span is static template text.

is_variable: bool

Whether this span came from a variable substitution.


class ValidationResult

ValidationResult(
    self,
    required: set[str] = ,
    optional: set[str] = ,
    extra: set[str] = ,
    invalid: dict[str, str] = ,
    _template: Any = None,
    _json_vars: str | None = None,
    _strict: bool = False
)

Result of template input validation.

Distinguishes between required and optional variables. Required variables are used "naked" in the template ({{ name }}), while optional variables have a default() filter ({{ context | default('') }}).

This distinction helps when loading third-party templates via PromptTemplate.from_chat_template() - you know which variables will break the template vs which will safely fall back to defaults.

The ValidationResult can also render the template directly, avoiding re-serialization of the variables that were already validated:

result = template.validate(documents=large_docs)
if result.is_valid:
    output = result.render()  # No re-serialization!
Attributes
required

Variables required by the template (no default) but not provided. Missing these will likely break template logic or produce empty output.

optional

Variables with default() filter that are not provided. These are safe to omit - the template handles missing values gracefully.

extra

Variables provided but not used by the template. These are warnings, not errors - extra variables are ignored.

invalid

Dictionary mapping variable names to error messages for values that cannot be serialized to JSON.

Example
>>> t = PromptTemplate("Hello {{ name }}! Context: {{ context | default('N/A') }}")
>>> result = t.validate(name="Alice")
>>> result.is_valid
True  # Only 'required' matters for validity
>>> result.required
set()  # name was provided
>>> result.optional
{'context'}  # context has default, safe to omit

# Efficient validate-then-render pattern: >>> result = t.validate(name="Alice") >>> if result: ... output = result.render() # Reuses serialized variables

Quick Reference

Properties

Name Type
is_valid bool
summary str

Methods

Method Description
render() Render the template using the validated variables.

Properties

is_valid: bool

Whether validation passed.

Returns True only if there are no missing required variables and no invalid (non-JSON-serializable) values.

Note

Optional and extra variables do NOT cause validation to fail. Only required variables and invalid values matter for validity.

summary: str

Human-readable summary of validation issues.

Returns empty string if validation passed.

Example
>>> result.summary
'Missing required variables: name. Missing optional variables (have defaults): context'

Methods

def render(self, strict: bool | None = None)str

Render the template using the validated variables.

This method reuses the JSON serialization from validation, avoiding the overhead of re-serializing large variables like RAG documents.

Parameters
strict

Override strict mode for this render only. If None (default), uses the mode from validation.

Returns

The rendered template string.

Raises
ValueError

If validation failed or result wasn't created by validate().

TemplateError

If rendering fails.

Example

Efficient validate-then-render for large documents:

>>> docs = load_large_documents()  # 10MB of data
>>> result = template.validate(documents=docs, question="What is...?")
>>> if result.is_valid:
...     output = result.render()  # No re-serialization!

With strict mode override:

>>> result = template.validate(name="Alice")
>>> output = result.render(strict=True)  # Force strict for this render