prompt-builder-py
Fluent prompt builder for LLM APIs — templates, variable substitution, token counting, and chat history management in one zero-dependency Python library.
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
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)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09801fd74088ff28d09d9c084b1de48edac63b479e965b31e61961dd715f83ed
|
|
| MD5 |
bbd8616215f536da5b0d3702084213fb
|
|
| BLAKE2b-256 |
a6732f924db5220c8759c1149a2b108b11e5f71520deaab91da39ef402821f62
|
File details
Details for the file prompt_builder_py-1.0.0-py3-none-any.whl.
File metadata
- Download URL: prompt_builder_py-1.0.0-py3-none-any.whl
- Upload date:
- Size: 23.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b419195ba3516e122a711f5c771c5a65e4ecb34b09c7d51fd89acae057d18933
|
|
| MD5 |
5e1050260b614bd24df2d90ee7ca0aeb
|
|
| BLAKE2b-256 |
001c1b172ba14a2e480ae0126433bd9dcc43407089005e44193f17fd58d7d5a3
|