A lightweight prompt assembly library using sigil-based substitution with no template logic
Project description
prompt-assemble
A lightweight prompt assembly library for building dynamic prompts with sigil-based substitution. No logic in templates — templates stay dumb, logic stays in Python.
Features
- Sigil-based substitution — Simple placeholder syntax (
[[VAR_NAME]]and[[PROMPT: name]]) - Format-agnostic — Works with loose XML, JSON, or plain text
- Recursive substitution — Variable values can contain sigils, resolved in a second pass
- Comments support — Single-line (
#!) and multiline (<!-- -->) comments - No template logic — Loops, conditionals, and transforms belong in Python
- Portable — Easy to use across different environments
Installation
pip install prompt-assemble
Quick Start
from prompt_assemble import assemble
template = """
<system>
You are a [[PROMPT: persona]] specializing in [[DOMAIN]].
</system>
<task>
[[PROMPT: task-instructions]]
</task>
<question>
[[QUESTION]]
</question>
"""
components = {
"persona": "expert software architect",
"task-instructions": "Analyze the code and provide recommendations.",
}
variables = {
"DOMAIN": "Python development",
"QUESTION": "How can we improve this function?",
}
result = assemble(template, components=components, variables=variables)
print(result)
Format Support
Loose XML
<system>You are a [[PROMPT: persona]]</system>
<task>[[PROMPT: task-instructions]]</task>
JSON
{
"system": "You are a [[PROMPT: persona]]",
"task": "[[PROMPT: task-instructions]]"
}
Plain Text
Subject: [[SUBJECT]]
Body:
[[BODY]]
Sigil Syntax
| Sigil | Purpose |
|---|---|
[[VAR_NAME]] |
Simple variable substitution |
[[PROMPT: name]] |
Inject a named prompt component |
[[PROMPT_TAG: tag1, tag2]] |
Inject all prompts matching tags (AND intersection) |
[[PROMPT_TAG:N: tag1, tag2]] |
Inject N most recent prompts matching tags |
Comments
#! Single line comment
<!-- Multi-line
comment -->
Comments are stripped before substitution and never reach the model.
Recursive Substitution
Variable values can themselves contain sigils:
variables = {
"TASK": "Analyze [[CODE_TYPE]] code",
"CODE_TYPE": "Python",
}
# Second pass resolves nested sigils
CLI Usage
pambl --template prompt.prompt --components components.json --variables vars.json
Database
PostgreSQL is required. The DatabaseSource implementation uses PostgreSQL-specific features including automatic reconnection on timeout and transaction management optimized for reliability.
PostgreSQL Support
- PostgreSQL 10+ (required for production and testing)
- Automatic connection reconnection on timeout
- Multi-tenant support via table prefixes
- Version history and tagging
Quick Setup with PostgreSQL
import psycopg2
from prompt_assemble.sources import DatabaseSource
# Connect to PostgreSQL
conn = psycopg2.connect(
host="localhost",
database="prompts",
user="postgres",
password="secret"
)
# Create source with table prefix for multi-tenant support
source = DatabaseSource(conn, table_prefix="prod_")
# Use with PromptProvider
from prompt_assemble import PromptProvider
provider = PromptProvider(source)
Docker Compose with PostgreSQL
See DOCKER.md for a complete Docker Compose setup with PostgreSQL.
Bulk Import (Preloading Database)
The bulk_import() function allows you to migrate all prompts and their metadata from one source to another. This is useful for:
- Populating a database with prompts stored in the filesystem
- Migrating between storage backends
- Backing up/restoring prompts
from prompt_assemble import (
PromptProvider,
FileSystemSource,
DatabaseSource,
bulk_import
)
import psycopg2
# Load prompts from filesystem
filesystem_source = FileSystemSource('./prompts')
source_provider = PromptProvider(filesystem_source)
# Connect to PostgreSQL database
conn = psycopg2.connect(
host="localhost",
database="prompts",
user="postgres",
password="secret"
)
database_source = DatabaseSource(conn, table_prefix="prod_")
target_provider = PromptProvider(database_source)
# Bulk import with all metadata (tags, description, owner)
results = bulk_import(source_provider, target_provider, verbose=True)
print(f"✅ Imported {results['imported']} prompts")
print(f"⚠️ Skipped {results['skipped']} existing prompts")
print(f"❌ Errors: {results['errors']}")
Bulk Import Options
# Skip prompts already in target
results = bulk_import(
source_provider,
target_provider,
skip_existing=True # Don't overwrite existing prompts
)
# Verbose output for debugging
results = bulk_import(
source_provider,
target_provider,
verbose=True # Log each import operation
)
Return Value
The bulk_import() function returns a dictionary with import statistics:
{
"imported": 42, # Successfully imported
"skipped": 3, # Skipped (already exist or skip_existing=True)
"errors": 1, # Failed imports
"errors_list": [ # Details of failures
{"name": "failing_prompt", "error": "Connection timeout"}
]
}
What Gets Transferred
All metadata is preserved during bulk import:
- ✅ Prompt content
- ✅ Tags (AND intersection filtering supported)
- ✅ Description
- ✅ Owner
- ✅ Versioning (for database targets)
Variable Sets
Variable sets allow you to manage reusable variable combinations and apply them across multiple prompts. Variables can optionally carry XML wrapper tags for structured output.
Creating and Using Variable Sets
from prompt_assemble import PromptProvider
from prompt_assemble.sources import FileSystemSource
provider = PromptProvider(FileSystemSource('./prompts'))
# Create a variable set
set_id = provider.create_variable_set(
name="persona_expert",
variables={
"ROLE": "expert software architect",
"TONE": "technical and precise"
}
)
# Render a prompt with variable sets
result = provider.render(
"my_prompt",
variable_sets=[set_id] # Use variables from the set
)
Tagged Variables (XML Output)
Variables can include optional XML wrapper tags:
# Create a variable set with tagged variables
set_id = provider.create_variable_set(
name="personas",
variables={
"ROLE": {"value": "expert", "tag": "persona"},
"DOMAIN": "Python development"
}
)
# The [[ROLE]] sigil renders as:
# <persona>
# expert
# </persona>
Merge Priority
When rendering, variables are merged in this order (later overrides earlier):
- Subscribed variable sets (sets linked to the prompt)
- Additional variable sets (passed via
variable_setsparam) - Per-prompt overrides (specific to this prompt + set combination)
- Explicit variables (passed to
render()- highest priority)
# Create two sets
set1 = provider.create_variable_set("set1", {"NAME": "Alice", "AGE": 30})
set2 = provider.create_variable_set("set2", {"NAME": "Bob"})
# Render with merge priority
result = provider.render(
"prompt",
variable_sets=[set2], # Additional set
variables={"NAME": "Charlie"} # Explicit var wins
)
# Result uses NAME=Charlie, AGE=30 (from set2)
Granular Variable Operations
Add or remove individual variables without replacing the entire set:
# Add a single variable (updates if exists)
provider.add_variable_to_set(set_id, "TOPIC", "machine learning", tag="domain")
# Remove a single variable
provider.remove_variable_from_set(set_id, "TOPIC")
Finding Variable Sets
Discover variable sets by name, owner, or both:
# Exact match
sets = provider.find_variable_sets(name="persona_expert")
# Partial match
sets = provider.find_variable_sets(name="persona", match_type="partial")
# By owner
sets = provider.find_variable_sets(owner="alice")
# Owner-scoped sets (global + owner's scoped sets)
sets = provider.get_available_variable_sets(owner="alice")
# List only global (unscoped) sets
sets = provider.list_global_variable_sets()
Subscriptions and Overrides
Link variable sets to specific prompts and override values:
# Subscribe a prompt to variable sets
provider.set_active_variable_sets("my_prompt", [set1_id, set2_id])
# Get subscribed sets
active = provider.get_active_variable_sets("my_prompt")
# Override specific variables for this prompt + set combination
provider.set_variable_overrides(
"my_prompt",
set1_id,
{"NAME": "Custom Override"}
)
Docker Deployment
Unified Docker Image (Backend + Frontend)
A pre-built unified image is available that includes both the Python backend and the React frontend in a single container. This is the easiest way to get started with the full application.
Image Name: ghcr.io/{owner}/prompt-assemble-with-ui
Quick Start with Unified Image
Run with filesystem backend (no database required):
docker run -p 8000:8000 \
-v ./prompts:/app/prompts \
ghcr.io/hominemAI/prompt-assemble-with-ui:latest
Then open http://localhost:8000 in your browser.
Run with PostgreSQL database:
docker run -p 8000:8000 \
-e DB_HOSTNAME=postgres.example.com \
-e DB_PORT=5432 \
-e DB_USERNAME=postgres \
-e DB_PASSWORD=your_secure_password \
-e DB_DATABASE=prompts \
-e PROMPT_ASSEMBLE_TABLE_PREFIX=prod_ \
ghcr.io/hominemAI/prompt-assemble-with-ui:latest
With Docker Compose (PostgreSQL):
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: prompts
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
prompt-assemble:
image: ghcr.io/hominemAI/prompt-assemble-with-ui:latest
ports:
- "8000:8000"
environment:
DB_HOSTNAME: postgres
DB_PORT: 5432
DB_USERNAME: postgres
DB_PASSWORD: secret
DB_DATABASE: prompts
PROMPT_ASSEMBLE_TABLE_PREFIX: prod_
depends_on:
- postgres
volumes:
postgres_data:
Building the Unified Image Locally
To build the unified image yourself:
docker build -f Dockerfile.unified -t prompt-assemble-with-ui:latest .
The Dockerfile.unified multi-stage build:
- Frontend stage: Clones and builds the frontend from HominemAI/prompt-assemble-ui
- Backend stage: Builds the Python prompt-assemble library
- Runtime stage: Combines both, serving everything on port 8000
GitHub Actions Workflow
The .github/workflows/build-image-with-ui.yml workflow automatically builds and publishes the unified image:
- Triggers: Pushes to
mainand tags matchingui-v*.*.* - Image: Published to GitHub Container Registry as
prompt-assemble-with-ui - Signing: Images are signed with cosign for security
To publish a release:
git tag ui-v1.0.0
git push origin ui-v1.0.0
Architecture
The unified image uses a single port for both frontend and backend:
- Port 8000: All traffic (frontend + API)
- Frontend: React UI served at
/ - API: REST endpoints at
/api/* - Frontend automatically configures: API base URL set to
/api(relative path)
What's Included
- Backend: Prompt Assembly library with all features (sigil substitution, versioning, variable sets)
- Frontend: React-based UI from prompt-assemble-ui
- Storage Options:
- Filesystem:
.promptfiles in/app/promptsdirectory - PostgreSQL: Full database backend with versioning and multi-tenancy
- Filesystem:
Environment Variables (Unified Image)
Same as regular backend, plus frontend-specific options:
# Backend API (port 8000)
FLASK_HOST=0.0.0.0
FLASK_PORT=8000
# Database (optional - defaults to filesystem)
DB_HOSTNAME=postgres
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=secret
DB_DATABASE=prompts
PROMPT_ASSEMBLE_TABLE_PREFIX=prod_
# Frontend (optional)
VITE_API_URL=/api # Default: /api (relative path to port 8000)
Environment Variables
The prompt-assemble library and UI support the following environment variables for configuration:
Starting the UI Server
Use the provided startup script:
python start_ui_db.py
This script automatically:
- Connects to PostgreSQL using environment variables
- Initializes the database schema
- Starts the Flask UI server
- Creates tables with the configured prefix
Database Configuration (PostgreSQL)
| Variable | Type | Default | Description |
|---|---|---|---|
DB_HOSTNAME |
string | localhost |
PostgreSQL server hostname |
DB_PORT |
int | 5432 |
PostgreSQL server port |
DB_USERNAME |
string | postgres |
PostgreSQL username |
DB_PASSWORD |
string | (required) | PostgreSQL password |
DB_DATABASE |
string | prompts |
PostgreSQL database name |
DB_SSLMODE |
string | require |
SSL mode: require, prefer, disable |
DB_PREFIX |
string | pambl_ |
Table name prefix (e.g., tables become pambl_prompts, pambl_prompt_tags) |
PORT |
int | 8000 |
Port for the Flask UI server |
Example - Local PostgreSQL:
export DB_HOSTNAME=localhost
export DB_PORT=5432
export DB_USERNAME=postgres
export DB_PASSWORD=your_password
export DB_DATABASE=prompts
export DB_PREFIX=pambl_
export PORT=8000
python start_ui_db.py
Example - DigitalOcean Managed PostgreSQL:
export DB_HOSTNAME=db-postgresql-sfo2-xxxx-do-user-xxxxx-0.e.db.ondigitalocean.com
export DB_PORT=25060
export DB_USERNAME=pambl_user
export DB_PASSWORD=your_secure_password
export DB_DATABASE=pambl_db
export DB_SSLMODE=require
export DB_PREFIX=pambl_
export PORT=8000
python start_ui_db.py
Programmatic Configuration (Legacy)
| Variable | Type | Default | Description |
|---|---|---|---|
PROMPT_ASSEMBLE_UI |
bool | false |
Enable/disable the web UI server. Set to "true" to activate |
PROMPT_ASSEMBLE_TABLE_PREFIX |
string | "" (empty) |
Table prefix (deprecated - use DB_PREFIX instead) |
Listener & Event Configuration
| Variable | Type | Default | Description |
|---|---|---|---|
| None currently | - | - | Listener callbacks are configured programmatically |
Configuration Examples
Development Environment (Local PostgreSQL)
export DB_HOSTNAME=localhost
export DB_PORT=5432
export DB_USERNAME=postgres
export DB_PASSWORD=dev_password
export DB_DATABASE=prompts_dev
export DB_SSLMODE=disable
export DB_PREFIX=dev_
export PORT=8000
python start_ui_db.py
Production (DigitalOcean PostgreSQL)
export DB_HOSTNAME=db-postgresql-sfo2-xxxxx-do-user-xxxxx-0.e.db.ondigitalocean.com
export DB_PORT=25060
export DB_USERNAME=prod_user
export DB_PASSWORD=your_secure_password
export DB_DATABASE=prompts_prod
export DB_SSLMODE=require
export DB_PREFIX=prod_
export PORT=8000
python start_ui_db.py
Testing
export DB_HOSTNAME=localhost
export DB_PORT=5432
export DB_USERNAME=postgres
export DB_PASSWORD=test_password
export DB_DATABASE=prompts_test
export DB_PREFIX=test_
export PORT=8001
python start_ui_db.py
Programmatic Configuration
You can also configure these settings directly in Python:
from prompt_assemble.sources import DatabaseSource
from prompt_assemble.api import run_server
import psycopg2
# Configure PostgreSQL database with table prefix
conn = psycopg2.connect(
host="localhost",
database="prompts",
user="postgres",
password="secret"
)
source = DatabaseSource(conn, table_prefix='myapp_')
# Configure Flask server
run_server(
source=source,
host='0.0.0.0',
port=8000,
debug=False
)
Quick Reference
All Environment Variables
# UI Server (Required for web interface)
PROMPT_ASSEMBLE_UI=true
# Flask Configuration (Optional)
FLASK_HOST=0.0.0.0
FLASK_PORT=8000
FLASK_DEBUG=false
# PostgreSQL Database Connection
DB_HOSTNAME=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=secret
DB_DATABASE=prompts
# Database Options
PROMPT_ASSEMBLE_TABLE_PREFIX=myapp_ # Table prefix for multi-tenancy
Database Driver
Install psycopg2 for PostgreSQL:
pip install psycopg2-binary
Connection Resilience
The DatabaseSource automatically handles connection timeouts and network interruptions:
- Automatic reconnection — Detects closed connections and reconnects transparently
- Health checks — Validates connection before each operation
- Timeout recovery — Handles PostgreSQL idle timeout gracefully
No configuration required — reconnection happens automatically on these operations:
save_prompt()- Save/update promptsdelete_prompt()- Delete promptsrefresh()- Reload metadataget_raw()- Fetch content- All variable set operations
Testing
Run the full test suite:
pytest tests/
Note: Database-specific tests require PostgreSQL:
# Skip database tests (for environments without PostgreSQL)
pytest tests/ -k "not test_database"
# Run only with PostgreSQL available
PGHOST=localhost PGUSER=postgres PGPASSWORD=secret PGDATABASE=test_prompts pytest tests/test_database_source.py
Test Coverage: 135 passing tests covering core library, FileSystem source, listener system, save/delete operations, and bulk import functionality. PostgreSQL-specific tests are marked as requiring PostgreSQL.
Contributing
Contributions welcome! Please open an issue or submit a pull request.
License
MIT License — see LICENSE file for details.
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_assemble-0.3.7.tar.gz.
File metadata
- Download URL: prompt_assemble-0.3.7.tar.gz
- Upload date:
- Size: 67.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ea749d32141120b0b45cec0b7c6444c19dd686d983b5aabf92d207cbe0380bb
|
|
| MD5 |
c4ebef6863b321b62338ef2cad73bbff
|
|
| BLAKE2b-256 |
7d21e414f7798f2af6b56123b92c8fc190e94afbd83200a85ba8ae06eadf4007
|
File details
Details for the file prompt_assemble-0.3.7-py3-none-any.whl.
File metadata
- Download URL: prompt_assemble-0.3.7-py3-none-any.whl
- Upload date:
- Size: 47.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b4dd11e815ae02061aafc7ae8cdb282d5a86e346e05ae2c18dce7666cbf5a12
|
|
| MD5 |
ac1e2aaccbe5687bb8b05bcc2528c9f6
|
|
| BLAKE2b-256 |
67d1fe71228790cc1f17bf849266256ff0272141d00768c7d18dcf1ece194f36
|