Skip to main content

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):

  1. Subscribed variable sets (sets linked to the prompt)
  2. Additional variable sets (passed via variable_sets param)
  3. Per-prompt overrides (specific to this prompt + set combination)
  4. 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:

  1. Frontend stage: Clones and builds the frontend from HominemAI/prompt-assemble-ui
  2. Backend stage: Builds the Python prompt-assemble library
  3. 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 main and tags matching ui-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: .prompt files in /app/prompts directory
    • PostgreSQL: Full database backend with versioning and multi-tenancy

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 prompts
  • delete_prompt() - Delete prompts
  • refresh() - Reload metadata
  • get_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

prompt_assemble-0.3.13.tar.gz (67.4 kB view details)

Uploaded Source

Built Distribution

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

prompt_assemble-0.3.13-py3-none-any.whl (47.7 kB view details)

Uploaded Python 3

File details

Details for the file prompt_assemble-0.3.13.tar.gz.

File metadata

  • Download URL: prompt_assemble-0.3.13.tar.gz
  • Upload date:
  • Size: 67.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for prompt_assemble-0.3.13.tar.gz
Algorithm Hash digest
SHA256 5629a98d0c7972b774ed8cfc9f1900e9a6d17f35ec2ee6b9bd117bb8ddfb5661
MD5 ac5df66f6daaeaa97bc1a0bb93a845e7
BLAKE2b-256 3a3a55253e19630f6f1de8e115c1048dae2e891acab8191fc18ab5b50e806741

See more details on using hashes here.

File details

Details for the file prompt_assemble-0.3.13-py3-none-any.whl.

File metadata

File hashes

Hashes for prompt_assemble-0.3.13-py3-none-any.whl
Algorithm Hash digest
SHA256 c7991c80a4986af8d6e59ef6749af36fe39b2148ede02ae24672fbc744dc429a
MD5 2ae14de55f9f6f181bac2b78e47dba79
BLAKE2b-256 c016c27f34f51d29eadd574b8553b4ea20ab41291f66a995003a4ffd0fab1500

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