A lightweight prompt assembly library for building dynamic prompts with sigil-based substitution. No logic in templates — templates stay dumb, logic stays in Python.
- 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
pip install prompt-assemblefrom 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)<system>You are a [[PROMPT: persona]]</system>
<task>[[PROMPT: task-instructions]]</task>{
"system": "You are a [[PROMPT: persona]]",
"task": "[[PROMPT: task-instructions]]"
}Subject: [[SUBJECT]]
Body:
[[BODY]]
| 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 |
#! Single line comment
<!-- Multi-line
comment -->
Comments are stripped before substitution and never reach the model.
Variable values can themselves contain sigils:
variables = {
"TASK": "Analyze [[CODE_TYPE]] code",
"CODE_TYPE": "Python",
}
# Second pass resolves nested sigilspambl --template prompt.prompt --components components.json --variables vars.jsonPostgreSQL is required. The DatabaseSource implementation uses PostgreSQL-specific features including automatic reconnection on timeout and transaction management optimized for reliability.
- PostgreSQL 10+ (required for production and testing)
- Automatic connection reconnection on timeout
- Multi-tenant support via table prefixes
- Version history and tagging
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)See DOCKER.md for a complete Docker Compose setup with PostgreSQL.
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']}")# 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
)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"}
]
}All metadata is preserved during bulk import:
- ✅ Prompt content
- ✅ Tags (AND intersection filtering supported)
- ✅ Description
- ✅ Owner
- ✅ Versioning (for database targets)
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.
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
)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>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)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")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()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"}
)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
Run with filesystem backend (no database required):
docker run -p 8000:8000 \
-v ./prompts:/app/prompts \
ghcr.io/hominemAI/prompt-assemble-with-ui:latestThen 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:latestWith 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: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
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.0The 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)
- 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:
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)The prompt-assemble library and UI support the following environment variables for configuration:
Use the provided startup script:
python start_ui_db.pyThis script automatically:
- Connects to PostgreSQL using environment variables
- Initializes the database schema
- Starts the Flask UI server
- Creates tables with the configured prefix
| 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.pyExample - 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| 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) |
| Variable | Type | Default | Description |
|---|---|---|---|
| None currently | - | - | Listener callbacks are configured programmatically |
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.pyexport 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.pyexport 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.pyYou 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
)# 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-tenancyInstall psycopg2 for PostgreSQL:
pip install psycopg2-binaryThe 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
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.pyTest 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.
Contributions welcome! Please open an issue or submit a pull request.
MIT License — see LICENSE file for details.