Categories

Archives

A sample text widget

Etiam pulvinar consectetur dolor sed malesuada. Ut convallis euismod dolor nec pretium. Nunc ut tristique massa.

Nam sodales mi vitae dolor ullamcorper et vulputate enim accumsan. Morbi orci magna, tincidunt vitae molestie nec, molestie at mi. Nulla nulla lorem, suscipit in posuere in, interdum non magna.

Prompt Template Management: Engineering Discipline for LLM Prompts

Introduction: Prompts are the interface between your application and LLMs. As applications grow, managing prompts becomes challenging—they’re scattered across code, hard to version, and difficult to test. A prompt template system brings order to this chaos. It separates prompt logic from application code, enables versioning and A/B testing, and makes prompts reusable across different contexts. This guide covers practical template management: designing flexible templates, implementing version control, building template registries, and creating systems that let you iterate on prompts without deploying code.

Prompt Template
Template Management: Template Storage, Version Control, Template Rendering

Template Design

from dataclasses import dataclass, field
from typing import Any, Optional, Callable
import re
from string import Template as StringTemplate

@dataclass
class PromptTemplate:
    """A prompt template with variables."""
    
    name: str
    template: str
    description: str = ""
    
    # Variable definitions
    variables: dict[str, dict] = field(default_factory=dict)
    
    # Metadata
    version: str = "1.0.0"
    tags: list[str] = field(default_factory=list)
    
    def render(self, **kwargs) -> str:
        """Render template with variables."""
        
        # Validate required variables
        for var_name, var_def in self.variables.items():
            if var_def.get("required", True) and var_name not in kwargs:
                if "default" in var_def:
                    kwargs[var_name] = var_def["default"]
                else:
                    raise ValueError(f"Missing required variable: {var_name}")
        
        # Apply transformations
        for var_name, value in kwargs.items():
            if var_name in self.variables:
                transform = self.variables[var_name].get("transform")
                if transform:
                    kwargs[var_name] = transform(value)
        
        # Render using Python format strings
        return self.template.format(**kwargs)
    
    def get_variables(self) -> list[str]:
        """Extract variable names from template."""
        
        pattern = r'\{(\w+)\}'
        return list(set(re.findall(pattern, self.template)))

class JinjaPromptTemplate:
    """Prompt template using Jinja2 for complex logic."""
    
    def __init__(
        self,
        name: str,
        template: str,
        description: str = ""
    ):
        from jinja2 import Environment, BaseLoader
        
        self.name = name
        self.template_str = template
        self.description = description
        
        self.env = Environment(loader=BaseLoader())
        self.template = self.env.from_string(template)
    
    def render(self, **kwargs) -> str:
        """Render template with Jinja2."""
        return self.template.render(**kwargs)

# Example templates
SUMMARIZATION_TEMPLATE = PromptTemplate(
    name="summarization",
    template="""Summarize the following text in {style} style.

Text:
{text}

Summary length: {length} words
Focus on: {focus}

Summary:""",
    description="Summarize text with configurable style and length",
    variables={
        "text": {"required": True, "description": "Text to summarize"},
        "style": {"required": False, "default": "concise", "description": "Writing style"},
        "length": {"required": False, "default": 100, "description": "Target word count"},
        "focus": {"required": False, "default": "key points", "description": "What to focus on"}
    },
    version="1.0.0",
    tags=["summarization", "text-processing"]
)

QA_TEMPLATE = PromptTemplate(
    name="question_answering",
    template="""Answer the question based on the provided context.

Context:
{context}

Question: {question}

Instructions:
- Only use information from the context
- If the answer is not in the context, say "I don't have enough information"
- Be {tone} in your response

Answer:""",
    description="Answer questions based on provided context",
    variables={
        "context": {"required": True},
        "question": {"required": True},
        "tone": {"required": False, "default": "helpful and professional"}
    },
    version="1.0.0",
    tags=["qa", "rag"]
)

Template Registry

from dataclasses import dataclass, field
from typing import Any, Optional
from datetime import datetime
import json

@dataclass
class TemplateVersion:
    """A specific version of a template."""
    
    version: str
    template: PromptTemplate
    created_at: datetime
    created_by: str = None
    changelog: str = None
    is_active: bool = True

class TemplateRegistry:
    """Registry for managing prompt templates."""
    
    def __init__(self):
        self._templates: dict[str, dict[str, TemplateVersion]] = {}
        self._active_versions: dict[str, str] = {}
    
    def register(
        self,
        template: PromptTemplate,
        created_by: str = None,
        changelog: str = None
    ) -> TemplateVersion:
        """Register a new template or version."""
        
        name = template.name
        version = template.version
        
        if name not in self._templates:
            self._templates[name] = {}
        
        # Create version entry
        template_version = TemplateVersion(
            version=version,
            template=template,
            created_at=datetime.utcnow(),
            created_by=created_by,
            changelog=changelog
        )
        
        self._templates[name][version] = template_version
        
        # Set as active if first version
        if name not in self._active_versions:
            self._active_versions[name] = version
        
        return template_version
    
    def get(
        self,
        name: str,
        version: str = None
    ) -> Optional[PromptTemplate]:
        """Get a template by name and optional version."""
        
        if name not in self._templates:
            return None
        
        if version is None:
            version = self._active_versions.get(name)
        
        if version not in self._templates[name]:
            return None
        
        return self._templates[name][version].template
    
    def set_active(self, name: str, version: str):
        """Set the active version for a template."""
        
        if name not in self._templates:
            raise ValueError(f"Template not found: {name}")
        
        if version not in self._templates[name]:
            raise ValueError(f"Version not found: {version}")
        
        self._active_versions[name] = version
    
    def list_templates(self) -> list[dict]:
        """List all templates with their active versions."""
        
        return [
            {
                "name": name,
                "active_version": self._active_versions.get(name),
                "versions": list(versions.keys()),
                "description": versions[self._active_versions.get(name)].template.description
                    if self._active_versions.get(name) else None
            }
            for name, versions in self._templates.items()
        ]
    
    def get_versions(self, name: str) -> list[TemplateVersion]:
        """Get all versions of a template."""
        
        if name not in self._templates:
            return []
        
        return sorted(
            self._templates[name].values(),
            key=lambda v: v.created_at,
            reverse=True
        )
    
    def render(
        self,
        name: str,
        version: str = None,
        **kwargs
    ) -> str:
        """Render a template by name."""
        
        template = self.get(name, version)
        
        if template is None:
            raise ValueError(f"Template not found: {name}")
        
        return template.render(**kwargs)

class FileTemplateRegistry(TemplateRegistry):
    """Registry that persists templates to files."""
    
    def __init__(self, base_path: str):
        super().__init__()
        self.base_path = base_path
        self._load_templates()
    
    def _load_templates(self):
        """Load templates from disk."""
        
        import os
        
        if not os.path.exists(self.base_path):
            os.makedirs(self.base_path)
            return
        
        for filename in os.listdir(self.base_path):
            if filename.endswith('.json'):
                filepath = os.path.join(self.base_path, filename)
                with open(filepath, 'r') as f:
                    data = json.load(f)
                    template = PromptTemplate(**data)
                    self.register(template)
    
    def save(self, template: PromptTemplate):
        """Save template to disk."""
        
        import os
        
        filename = f"{template.name}_{template.version}.json"
        filepath = os.path.join(self.base_path, filename)
        
        with open(filepath, 'w') as f:
            json.dump({
                "name": template.name,
                "template": template.template,
                "description": template.description,
                "variables": template.variables,
                "version": template.version,
                "tags": template.tags
            }, f, indent=2)

Template Composition

from dataclasses import dataclass
from typing import Any, Optional, Callable

@dataclass
class TemplateComponent:
    """A reusable template component."""
    
    name: str
    content: str
    description: str = ""

class ComposableTemplate:
    """Template that can include other templates."""
    
    def __init__(
        self,
        name: str,
        template: str,
        components: dict[str, TemplateComponent] = None
    ):
        self.name = name
        self.template = template
        self.components = components or {}
    
    def add_component(self, component: TemplateComponent):
        """Add a reusable component."""
        self.components[component.name] = component
    
    def render(self, **kwargs) -> str:
        """Render template with components."""
        
        # First, expand component references
        expanded = self.template
        
        for comp_name, component in self.components.items():
            placeholder = f"{{{{component:{comp_name}}}}}"
            expanded = expanded.replace(placeholder, component.content)
        
        # Then render variables
        return expanded.format(**kwargs)

# Example: Composable system prompt
PERSONA_COMPONENT = TemplateComponent(
    name="persona",
    content="""You are a helpful AI assistant with expertise in {domain}.
You communicate in a {tone} manner and always provide accurate information."""
)

SAFETY_COMPONENT = TemplateComponent(
    name="safety",
    content="""Important guidelines:
- Never provide harmful or dangerous information
- Acknowledge uncertainty when appropriate
- Recommend professional help for serious matters"""
)

OUTPUT_FORMAT_COMPONENT = TemplateComponent(
    name="output_format",
    content="""Format your response as follows:
- Use clear headings for different sections
- Include examples where helpful
- Keep explanations concise but complete"""
)

class TemplateBuilder:
    """Builder for constructing complex templates."""
    
    def __init__(self, name: str):
        self.name = name
        self._parts: list[str] = []
        self._variables: dict[str, dict] = {}
    
    def add_section(self, title: str, content: str) -> "TemplateBuilder":
        """Add a section to the template."""
        
        self._parts.append(f"## {title}\n{content}")
        return self
    
    def add_variable(
        self,
        name: str,
        description: str = "",
        required: bool = True,
        default: Any = None
    ) -> "TemplateBuilder":
        """Define a variable."""
        
        self._variables[name] = {
            "description": description,
            "required": required,
            "default": default
        }
        return self
    
    def add_component(self, component: TemplateComponent) -> "TemplateBuilder":
        """Add a component."""
        
        self._parts.append(component.content)
        return self
    
    def build(self) -> PromptTemplate:
        """Build the final template."""
        
        template_str = "\n\n".join(self._parts)
        
        return PromptTemplate(
            name=self.name,
            template=template_str,
            variables=self._variables
        )

# Example usage
def build_assistant_template() -> PromptTemplate:
    return (
        TemplateBuilder("assistant")
        .add_section("Role", "You are {role} specializing in {specialty}.")
        .add_variable("role", "The assistant's role", required=True)
        .add_variable("specialty", "Area of expertise", required=True)
        .add_component(SAFETY_COMPONENT)
        .add_section("Task", "{task}")
        .add_variable("task", "The user's task", required=True)
        .add_component(OUTPUT_FORMAT_COMPONENT)
        .build()
    )

Template Testing

from dataclasses import dataclass
from typing import Any, Optional, Callable
import re

@dataclass
class TestCase:
    """A test case for a template."""
    
    name: str
    variables: dict[str, Any]
    expected_contains: list[str] = None
    expected_not_contains: list[str] = None
    expected_length_range: tuple[int, int] = None

@dataclass
class TestResult:
    """Result of a template test."""
    
    test_name: str
    passed: bool
    rendered: str = None
    errors: list[str] = None

class TemplateValidator:
    """Validate prompt templates."""
    
    def validate_syntax(self, template: PromptTemplate) -> list[str]:
        """Validate template syntax."""
        
        errors = []
        
        # Check for balanced braces
        open_count = template.template.count('{')
        close_count = template.template.count('}')
        
        if open_count != close_count:
            errors.append(f"Unbalanced braces: {open_count} open, {close_count} close")
        
        # Check that all variables are defined
        used_vars = set(re.findall(r'\{(\w+)\}', template.template))
        defined_vars = set(template.variables.keys())
        
        undefined = used_vars - defined_vars
        if undefined:
            errors.append(f"Undefined variables: {undefined}")
        
        unused = defined_vars - used_vars
        if unused:
            errors.append(f"Unused variables: {unused}")
        
        return errors
    
    def validate_rendering(
        self,
        template: PromptTemplate,
        test_cases: list[TestCase]
    ) -> list[TestResult]:
        """Validate template rendering with test cases."""
        
        results = []
        
        for test in test_cases:
            errors = []
            rendered = None
            
            try:
                rendered = template.render(**test.variables)
                
                # Check expected content
                if test.expected_contains:
                    for expected in test.expected_contains:
                        if expected not in rendered:
                            errors.append(f"Missing expected content: {expected}")
                
                if test.expected_not_contains:
                    for unexpected in test.expected_not_contains:
                        if unexpected in rendered:
                            errors.append(f"Contains unexpected content: {unexpected}")
                
                # Check length
                if test.expected_length_range:
                    min_len, max_len = test.expected_length_range
                    if not (min_len <= len(rendered) <= max_len):
                        errors.append(
                            f"Length {len(rendered)} outside range [{min_len}, {max_len}]"
                        )
                
            except Exception as e:
                errors.append(f"Rendering error: {str(e)}")
            
            results.append(TestResult(
                test_name=test.name,
                passed=len(errors) == 0,
                rendered=rendered,
                errors=errors if errors else None
            ))
        
        return results

class TemplateTestSuite:
    """Test suite for templates."""
    
    def __init__(self, registry: TemplateRegistry):
        self.registry = registry
        self.validator = TemplateValidator()
        self._test_cases: dict[str, list[TestCase]] = {}
    
    def add_test(self, template_name: str, test: TestCase):
        """Add a test case for a template."""
        
        if template_name not in self._test_cases:
            self._test_cases[template_name] = []
        
        self._test_cases[template_name].append(test)
    
    def run_all(self) -> dict[str, list[TestResult]]:
        """Run all tests."""
        
        results = {}
        
        for template_name, tests in self._test_cases.items():
            template = self.registry.get(template_name)
            
            if template is None:
                results[template_name] = [TestResult(
                    test_name="template_exists",
                    passed=False,
                    errors=["Template not found"]
                )]
                continue
            
            # Syntax validation
            syntax_errors = self.validator.validate_syntax(template)
            
            if syntax_errors:
                results[template_name] = [TestResult(
                    test_name="syntax",
                    passed=False,
                    errors=syntax_errors
                )]
                continue
            
            # Run test cases
            results[template_name] = self.validator.validate_rendering(
                template, tests
            )
        
        return results

Production Template Service

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# Initialize registry
registry = TemplateRegistry()

# Register default templates
registry.register(SUMMARIZATION_TEMPLATE)
registry.register(QA_TEMPLATE)

class CreateTemplateRequest(BaseModel):
    name: str
    template: str
    description: str = ""
    variables: dict = {}
    version: str = "1.0.0"
    tags: list[str] = []

class RenderRequest(BaseModel):
    template_name: str
    version: Optional[str] = None
    variables: dict

@app.post("/v1/templates")
async def create_template(request: CreateTemplateRequest):
    """Create a new template."""
    
    template = PromptTemplate(
        name=request.name,
        template=request.template,
        description=request.description,
        variables=request.variables,
        version=request.version,
        tags=request.tags
    )
    
    # Validate syntax
    validator = TemplateValidator()
    errors = validator.validate_syntax(template)
    
    if errors:
        raise HTTPException(status_code=400, detail={"errors": errors})
    
    version = registry.register(template)
    
    return {
        "name": template.name,
        "version": template.version,
        "created_at": version.created_at.isoformat()
    }

@app.get("/v1/templates")
async def list_templates():
    """List all templates."""
    return {"templates": registry.list_templates()}

@app.get("/v1/templates/{name}")
async def get_template(name: str, version: Optional[str] = None):
    """Get a template by name."""
    
    template = registry.get(name, version)
    
    if template is None:
        raise HTTPException(status_code=404, detail="Template not found")
    
    return {
        "name": template.name,
        "template": template.template,
        "description": template.description,
        "variables": template.variables,
        "version": template.version,
        "tags": template.tags
    }

@app.get("/v1/templates/{name}/versions")
async def get_versions(name: str):
    """Get all versions of a template."""
    
    versions = registry.get_versions(name)
    
    return {
        "versions": [
            {
                "version": v.version,
                "created_at": v.created_at.isoformat(),
                "created_by": v.created_by,
                "changelog": v.changelog,
                "is_active": v.version == registry._active_versions.get(name)
            }
            for v in versions
        ]
    }

@app.post("/v1/templates/{name}/activate/{version}")
async def activate_version(name: str, version: str):
    """Set the active version for a template."""
    
    try:
        registry.set_active(name, version)
        return {"activated": True}
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))

@app.post("/v1/render")
async def render_template(request: RenderRequest):
    """Render a template with variables."""
    
    try:
        rendered = registry.render(
            request.template_name,
            request.version,
            **request.variables
        )
        
        return {
            "rendered": rendered,
            "template_name": request.template_name,
            "version": request.version or registry._active_versions.get(request.template_name)
        }
        
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/health")
async def health():
    return {"status": "healthy"}

References

Conclusion

Prompt template management brings engineering discipline to prompt development. Start with a clear template structure that separates static content from dynamic variables. Use a registry to centralize templates and enable versioning—this lets you roll back problematic changes and A/B test improvements. Build templates from reusable components for consistency across your application. Implement validation to catch syntax errors before they reach production. Test templates with representative inputs to ensure they render correctly. The key insight is that prompts are code—they deserve the same version control, testing, and deployment practices as your application code. A well-designed template system lets you iterate on prompts rapidly while maintaining reliability. As your LLM application grows, good template management becomes essential for maintaining quality and enabling collaboration across teams.