Skip to main content

A simple type-safe, validated prompt management system for LLMs

Project description

typed-prompt

A type-safe, validated prompt management system for LLMs that catches errors early, enforces type safety, and provides a structured way to manage prompts. Uses Pydantic models for variable validation and Jinja2 templates for prompt rendering.

Note: This library is in early development and subject to change.

Why typed-prompt?

I have always found it challenging to manage dynamic prompts for LLMs. The process is error-prone, with issues often discovered only at runtime. typed-prompt aims to solve this problem by providing a structured, type-safe way to manage prompts that catches errors early and enforces type safety.

Disclaimer: This is a personal project to solve gripes ive had in the past and not affiliated with any organization. It is a work in progress and subject to change.

I will be adding more features and examples in the future. If you have any suggestions or feedback, feel free to open an issue!

Quick Examples

1. Basic Usage with Validation

from typed_prompt import BasePrompt
from pydantic import BaseModel
from typing import Optional

# Define your variables
class UserVars(BaseModel):
    name: str
    expertise: str

# This works - all template variables are defined
class ValidPrompt(BasePrompt[UserVars]):
    """Helping {{name}} with {{expertise}} level knowledge."""
    prompt_template: str = "Explain {{topic}} to me"
    variables: UserVars

    def render(self, *, topic: str, **extra_vars) -> RenderOutput:
        extra_vars["topic"] = topic
        return super().render(**extra_vars)

# This fails immediately - 'unknown_var' not defined
class InvalidPrompt(BasePrompt[UserVars]):
    prompt_template: str = "What is {{unknown_var}}?"  # ValueError!
    variables: UserVars

# This fails - 'expertise' defined but never used
class UnusedVarPrompt(BasePrompt[UserVars]):
    prompt_template: str = "Hello {{name}}"  # ValueError!
    variables: UserVars

2. Custom Configuration

from typed_prompt import RenderOutput
from pydantic import BaseModel, Field


class MyConfig(BaseModel):
    temperature: float = Field(default=0.7, ge=0, le=2)
    model: str = Field(default="gpt-4")

class MyPrompt(BasePrompt[UserVars]):
    """Assistant for {{name}}"""
    prompt_template: str = "Help with {{topic}}"
    variables: UserVars
    config: MyConfig = Field(default_factory=MyConfig)

    def render(self, *, topic: str, **extra_vars) -> RenderOutput:
        extra_vars["topic"] = topic
        return super().render(**extra_vars)

# Use custom config
prompt = MyPrompt(
    variables=UserVars(name="Alice", expertise="intermediate"),
    config=MyConfig(temperature=0.9, model="gpt-3.5-turbo")
)

3. Conditional Templates

from typing import Union

class TemplateVars(BaseModel):
    user_type: Union["expert", "beginner"]
    name: str
    preferences: Optional[dict] = None

class ConditionalPrompt(BasePrompt[TemplateVars]):
    """{% if user_type == 'expert' %}
    Technical advisor for {{name}}
    {% else %}
    Friendly helper for {{name}}
    {% endif %}"""

    prompt_template: str = """
    {% if preferences %}
    Considering your preferences: {% for k, v in preferences.items() %}
    - {{k}}: {{v}}{% endfor %}
    {% endif %}
    How can I help with {{topic}}?
    """
    variables: TemplateVars

    def render(self, *, topic: str, **extra_vars) -> RenderOutput:
        extra_vars["topic"] = topic
        return super().render(**extra_vars)

Note: Using None as a value for optional variables will render as None in the prompt. e.g "Test example {{var}} will render as Test example None if var is None. This is the default behaviour of jinja. Therefore you need to handle this in your jinja2 template. e.g {{if var}} or {{var | default('default value')}} or however you want to handle it.

Key Features

Early Validation

The library validates your prompt templates during class definition:

  • Missing variables are caught immediately
  • Unused variables are detected
  • Template syntax is verified
  • Type checking is enforced

Type Safety

All variables are validated through Pydantic:

  • Required vs optional fields
  • Type constraints
  • Custom validators
  • Nested models

Flexible Configuration

Attach custom configuration to prompts:

  • Model parameters
  • Custom settings
  • Validation rules
  • Default values

Why Early Validation Matters

Consider this example:

# Without typed-prompt
def create_prompt(user_data):
    template = "Hello {{username}}, your level is {{level}}"
    # Error only discovered when rendering with wrong data
    return template.format(**user_data)  # KeyError at runtime!

# With typed-prompt
class UserPrompt(BasePrompt[UserVars]):
    prompt_template: str = "Hello {{unknown_var}}"  # Error immediately!
    variables: UserVars

The library catches template errors at definition time.

Installation

uv add tpyed-prompt

or

pip install typed-prompt

Examples

For more examples and detailed documentation, check the examples directory.

To run the examples:

uv run python examples/user.py

Core Concepts

The Prompt Structure

typed-prompt uses a two-part prompt structure that matches common LLM interaction patterns:

  1. System Prompt: Provides context or instructions for the AI model. You can define this in two ways:

    • As a class docstring (recommended for better code organization)
    • As a system_prompt_template class attribute
  2. User Prompt: Contains the actual prompt template that will be sent to the model. This is always defined in the prompt_template class attribute.

Variable Management

Variables in typed-prompt are handled through three complementary mechanisms:

  1. Variables Model: A Pydantic model that defines the core variables your prompt needs:

    class UserVariables(BaseModel):
        name: str
        age: int
        occupation: Optional[str] = None
    
  2. Render Method Parameters: Additional variables can be defined as keyword-only arguments in a custom render method:

    def render(self, *, learning_topic: str, **extra_vars) -> RenderOutput:
        extra_vars["learning_topic"] = learning_topic
        return super().render(**extra_vars)
    
  3. Extra Variables: One-off variables can be passed directly to the render method.

Template Validation

The library performs comprehensive validation to catch common issues early:

  1. Missing Variables: Ensures all variables used in templates are defined either in the variables model or render method
  2. Unused Variables: Identifies variables that are defined but never used in templates
  3. Template Syntax: Validates Jinja2 template syntax at class definition time
  4. Type Checking: Leverages Pydantic's type validation for all variables

Working with External Templates

For complex prompts, you can load templates from external files:

class ComplexPrompt(BasePrompt[ComplexVariables]):
    with open("templates/system_prompt.j2") as f:
        system_prompt_template = f.read()

    with open("templates/user_prompt.j2") as f:
        prompt_template = f.read()

API Reference

BasePrompt[T]

The foundational class for creating structured prompts.

Type Parameters

  • T: A Pydantic BaseModel subclass defining the structure of template variables

Class Attributes

  • system_prompt_template: Optional[str] - System prompt template
  • prompt_template: str - User prompt template
  • variables: T - Instance of the variables model

Methods

  • render(**extra_vars) -> RenderOutput: Renders both prompts with provided variables

RenderOutput

A NamedTuple providing structured access to rendered prompts:

  • system_prompt: Optional[str] - The rendered system prompt
  • user_prompt: str - The rendered user prompt

Best Practices

Template Organization

Structure your templates for maximum readability and maintainability:

  1. Use Docstrings for System Prompts: When possible, define system prompts in class docstrings for better code organization:

    class UserPrompt(BasePrompt[UserVariables]):
        """You are having a conversation with {{name}}, a {{age}}-year-old {{occupation}}."""
        prompt_template: str = "What would you like to discuss?"
    
  2. Separate Complex Templates: For longer templates, use external files:

    with open("templates/system_prompt.j2") as f:
        system_prompt_template = f.read()
    

Common Patterns

Conditional Content

Use Jinja2's conditional syntax for dynamic content:

class DynamicPrompt(BasePrompt[Variables]):
    prompt_template: str = """
    {% if expert_mode %}
    Provide a detailed technical explanation of {{topic}}
    {% else %}
    Explain {{topic}} in simple terms
    {% endif %}
    """

Contributing

Contributions are welcome!

License

This project is licensed under the MIT License - see the LICENSE file for details.

TODO

  • Optionals will still render as None in the prompt.

  • Make Jinja2 optional, (for very simple templating just use string formatting e.g f"Hello {name}"). Maybe shoulda started simpler lol.

  • Output OpenAI compatible Message objects.

  • The ability to define, not just a system prompt and a single prompt, but prompt chains. eg system_prompt -> user_prompt -> assistant_response -> user_prompt -> assistant_response -> ...

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typed_prompt-0.1.1.tar.gz (46.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typed_prompt-0.1.1-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

File details

Details for the file typed_prompt-0.1.1.tar.gz.

File metadata

  • Download URL: typed_prompt-0.1.1.tar.gz
  • Upload date:
  • Size: 46.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.5.7

File hashes

Hashes for typed_prompt-0.1.1.tar.gz
Algorithm Hash digest
SHA256 52b9494f5c671e4aaaed5d1001d960db769b30cf6a0d4b1f00575510ecadd0f5
MD5 d9293ab300d8f60d189327ea3e6393f5
BLAKE2b-256 09308f1650a001c3a9413e2708e88f90c6fb995ac475f1e4183263437e79655c

See more details on using hashes here.

File details

Details for the file typed_prompt-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for typed_prompt-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 97919f36668636c8b6d75fd4194e9305b008224891e665fc592dac4472407de9
MD5 b7c457672f6e93bd1d9d55f45540bf04
BLAKE2b-256 fbbcecbdfeb03e09af411b5fc7ac4e25cb7e400b6cf67ea665de0f7de98fceeb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page