Skip to main content

Fluent prompt builder for LLM APIs - templates, variables, token counting, chat management

Project description

prompt-builder-py

Fluent prompt builder for LLM APIs — templates, variable substitution, token counting, and chat history management in one zero-dependency Python library.

PyPI version Python 3.9+ License: MIT

Installation

pip install prompt-builder-py

Why prompt-builder-py?

  • Fluent API — chain .system(), .user(), .assistant(), and .var() calls naturally.
  • Variable substitution{name} placeholders resolved at build time, not at declaration.
  • Handlebars templates{{variable}}, {{#if}}, {{#each}}, partials, and a global registry.
  • Chat history — add messages, auto-trim to token/count limits, export to JSON/Markdown.
  • Provider formatters — emit the exact payload shape for OpenAI, Anthropic, Cohere, Mistral, or plain text.
  • Token counting — dependency-free approximation using a hybrid char/word heuristic.
  • Zero dependencies — pure Python 3.9+, stdlib only.

Quick Start

from prompt_builder_py import PromptBuilder, OpenAIFormatter

messages = (
    PromptBuilder()
    .system("You are an expert in {domain}.")
    .user("Explain {concept} with a short example.")
    .var("domain", "Python")
    .var("concept", "decorators")
    .build()
)

payload = OpenAIFormatter(model="gpt-4o").format(messages, max_tokens=512)
# → {"model": "gpt-4o", "messages": [...], "max_tokens": 512}

PromptBuilder

Building messages

from prompt_builder_py import PromptBuilder

builder = (
    PromptBuilder()
    .system("You are a {role}.")
    .user("What is {topic}?")
    .assistant("Here is what I know about {topic}…")
    .user("Can you elaborate?")
)

Variable binding

# Bind one at a time
builder.var("role", "historian").var("topic", "the Roman Empire")

# Or bind many at once
builder.vars(role="historian", topic="the Roman Empire")
builder.vars({"role": "historian", "topic": "the Roman Empire"})

Strict mode

# Raise VariableNotFoundError for any unbound {variable}
builder.strict()

from prompt_builder_py import VariableNotFoundError
try:
    builder.build()
except VariableNotFoundError as e:
    print(e.variable_name)

Forking a builder

base = (
    PromptBuilder()
    .system("Answer in {language}.")
    .user("{question}")
    .strict()
)

en = base.copy().vars(language="English", question="What is entropy?")
fr = base.copy().vars(language="French",  question="What is entropy?")

print(en.build())
print(fr.build())

Output methods

Method Returns
.build() list[dict] — all messages as {role, content} dicts
.build_chat() list[dict] — non-system messages only
.build_system() str | None — rendered system message
.build_string() str — all messages concatenated
.build_last_user() str | None — last user message
.token_count(model) int — estimated token count
.missing_vars() list[str] — unbound variable names

Template Engine

Uses {{double-brace}} syntax to distinguish templates from builder variables.

from prompt_builder_py import Template, TemplateRegistry

# Basic substitution with optional default
t = Template("Hello, {{name}}! You have {{count|0}} messages.")
print(t.render(name="Alice", count=3))
# Hello, Alice! You have 3 messages.

print(t.render(name="Bob"))
# Hello, Bob! You have 0 messages.

Conditionals

t = Template(
    "{{#if premium}}You have unlimited access.{{/if}}"
    "{{#unless premium}}Upgrade to unlock all features.{{/unless}}"
)
print(t.render(premium=True))   # You have unlimited access.
print(t.render(premium=False))  # Upgrade to unlock all features.

Loops

t = Template(
    "Languages:\n{{#each langs}}- {{@index}}. {{.}}\n{{/each}}"
)
print(t.render(langs=["Python", "Go", "Rust"]))
# Languages:
# - 0. Python
# - 1. Go
# - 2. Rust

# Loop over dicts
t2 = Template("{{#each users}}{{name}} ({{role}})\n{{/each}}")
print(t2.render(users=[
    {"name": "Alice", "role": "admin"},
    {"name": "Bob",   "role": "viewer"},
]))

Partials and registry

TemplateRegistry.register("footer", "\n---\nprompt-builder-py")
TemplateRegistry.register(
    "reply",
    "Answer in {{lang|English}}:\n{{question}}\n{{> footer}}"
)

result = TemplateRegistry.render("reply", question="What is Python?", lang="French")

Introspection

t = Template("Hello {{name}}, your score is {{score|N/A}}.")
print(t.required_vars())  # ["name"]
print(t.optional_vars())  # ["score"]
print(t.validate({"score": 42}))  # ["name"]  ← missing

Chat History

from prompt_builder_py import ChatHistory

history = ChatHistory(max_tokens=4096, model="gpt-4o")
history.add_system("You are a concise assistant.")
history.add_user("What is Python?")
history.add_assistant("A high-level, interpreted programming language.")
history.add_user("And Rust?")

print(len(history))          # 4
print(history.token_count()) # ≈ estimated tokens

# Export
print(history.to_markdown())
json_str = history.to_json()

# Restore from JSON
history2 = ChatHistory.from_json(json_str, max_tokens=4096)

Truncation strategies

# oldest (default): drop oldest non-system messages
history = ChatHistory(max_messages=10, truncation_strategy="oldest")

# pairs: drop oldest user/assistant exchange pairs
history = ChatHistory(max_tokens=2048, truncation_strategy="pairs")

# middle: keep first and last messages, drop the middle
history = ChatHistory(max_tokens=2048, truncation_strategy="middle")

# Manual truncation
history.truncate(strategy="oldest")

Search

results = history.search("Python")
for msg in results:
    print(msg.role, msg.content[:50])

Token Counting

from prompt_builder_py import TokenCounter, TokenBudget

counter = TokenCounter(model="gpt-4o")
print(counter.count("Hello, world!"))           # ≈ 4
print(counter.count_messages([
    {"role": "user", "content": "Hello there"}
]))                                              # ≈ 11

# Truncate a long string to fit in a budget
short = counter.truncate_to_budget(long_text, budget=1000)

# Split into chunks
chunks = counter.split_by_budget(long_text, budget=500)
budget = TokenBudget(max_tokens=8192, model="gpt-4o")
budget.consume("System prompt text here")
budget.consume_messages(messages)
print(budget.remaining)    # tokens left
print(budget.usage_pct)    # e.g. 23.4
print(budget.is_exhausted) # False

Provider Formatters

OpenAI

from prompt_builder_py import OpenAIFormatter

payload = OpenAIFormatter(model="gpt-4o").format(
    messages,
    max_tokens=1024,
    temperature=0.7,
    response_format={"type": "json_object"},
)

Anthropic

from prompt_builder_py import AnthropicFormatter

payload = AnthropicFormatter(model="claude-sonnet-4-5").format(
    messages,
    max_tokens=2048,
    system="Override system prompt here",
)

Cohere

from prompt_builder_py import CohereFormatter

payload = CohereFormatter(model="command-r-plus").format(messages, max_tokens=512)

Mistral

from prompt_builder_py import MistralFormatter

payload = MistralFormatter(model="mistral-large-latest").format(messages, max_tokens=1024)

Plain text

from prompt_builder_py import TextFormatter

payload = TextFormatter().format(messages, separator="\n\n", role_prefix=True)
print(payload["text"])

Validation

from prompt_builder_py import validate_messages, MessageValidator, ValidationResult

# Simple helper — raises ValidationError on failure
validate_messages(messages, provider="anthropic", max_tokens=4096)

# Full control
result: ValidationResult = MessageValidator().validate(
    messages,
    provider="openai",
    max_tokens=8192,
    strict_alternation=True,
    require_user_first=True,
)
print(result.is_valid)   # True / False
print(result.errors)     # list of error strings
print(result.warnings)   # list of warning strings
result.raise_if_invalid()

Complete Example

from prompt_builder_py import (
    PromptBuilder,
    ChatHistory,
    Template,
    TemplateRegistry,
    AnthropicFormatter,
    TokenBudget,
    validate_messages,
)

# 1. Register reusable templates
TemplateRegistry.register(
    "code_review",
    "Review the following {{lang|Python}} code for correctness, "
    "performance, and security.\\n\\n```{{lang|python}}\\n{{code}}\\n```"
    "{{#if focus}}\\n\\nFocus on: {{focus}}{{/if}}"
)

# 2. Build the prompt
builder = (
    PromptBuilder()
    .system("You are a senior software engineer conducting a thorough code review.")
    .user(TemplateRegistry.render(
        "code_review",
        lang="Python",
        code="def add(a, b): return a + b",
        focus="type safety",
    ))
    .strict()
)

# 3. Track token usage
budget = TokenBudget(max_tokens=4096)
messages = builder.build()
budget.consume_messages(messages)
print(f"Tokens used: {budget.used} / {budget.max_tokens} ({budget.usage_pct}%)")

# 4. Validate before sending
validate_messages(messages, provider="anthropic", max_tokens=4096)

# 5. Format for Anthropic
payload = AnthropicFormatter().format(messages, max_tokens=1024)

# 6. Persist the conversation
history = ChatHistory(max_tokens=4096)
history.extend(messages)
history.add_assistant("The code looks correct. Consider adding type hints…")
print(history.to_markdown())

API Reference

PromptBuilder

Method Description
.system(content, **meta) Append a system message
.user(content, **meta) Append a user message
.assistant(content, **meta) Append an assistant message
.add(role, content, **meta) Append a message with any role
.extend(messages) Append a list of dicts
.var(name, value) Bind one variable
.vars(mapping, **kw) Bind multiple variables
.unset(*names) Remove variable bindings
.strict(enabled) Toggle strict variable mode
.copy() Deep-copy the builder
.reset() Clear all state
.build() Build all messages
.build_chat() Build non-system messages
.build_system() Get rendered system prompt
.build_string(sep) Build as single string
.token_count(model) Estimate token count
.missing_vars() List unbound variables

Template

Method Description
.render(context, **kw) Render with context
.required_vars() Variables with no default
.optional_vars() Variables with defaults
.all_vars() All referenced variables
.validate(context) List missing required vars

ChatHistory

Method Description
.add_system(content) Set the system message
.add_user(content) Append a user message
.add_assistant(content) Append an assistant message
.extend(messages) Append a list of dicts
.to_messages() Export as list[dict]
.to_json() Serialize to JSON string
.from_json(data) Deserialize from JSON
.to_markdown() Format as Markdown
.search(query) Find messages by content
.token_count() Estimate token count
.truncate(strategy) Trim to configured limits

License

MIT © Vladyslav Zaiets

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

prompt_builder_py-1.0.0.tar.gz (19.1 kB view details)

Uploaded Source

Built Distribution

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

prompt_builder_py-1.0.0-py3-none-any.whl (23.7 kB view details)

Uploaded Python 3

File details

Details for the file prompt_builder_py-1.0.0.tar.gz.

File metadata

  • Download URL: prompt_builder_py-1.0.0.tar.gz
  • Upload date:
  • Size: 19.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for prompt_builder_py-1.0.0.tar.gz
Algorithm Hash digest
SHA256 09801fd74088ff28d09d9c084b1de48edac63b479e965b31e61961dd715f83ed
MD5 bbd8616215f536da5b0d3702084213fb
BLAKE2b-256 a6732f924db5220c8759c1149a2b108b11e5f71520deaab91da39ef402821f62

See more details on using hashes here.

File details

Details for the file prompt_builder_py-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for prompt_builder_py-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b419195ba3516e122a711f5c771c5a65e4ecb34b09c7d51fd89acae057d18933
MD5 5e1050260b614bd24df2d90ee7ca0aeb
BLAKE2b-256 001c1b172ba14a2e480ae0126433bd9dcc43407089005e44193f17fd58d7d5a3

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