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.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
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
|