Skip to main content

Registry-based prompt model: sections of items, an assembly order, runtime modes, and a browser studio that exports the same JSON the library loads.

Project description

promptlibretto

A prompt-engineering library plus a browser studio for designing, tuning, and exporting prompts as portable JSON.

A registry is a flat list of named sections (personas, sentiment, examples, prompt_endings, …); each section holds items with their own fields. An assembly_order of tokens (section / section.field / section[expr]) tells the engine how to weave the selected items into one prompt. Per-array runtime modes (all / index:N / random:K / none), section-level random pickers, optional pre_context headings, and conditional text fragments handle all the small "don't show this if the var is empty" details that otherwise pile up as f-string spaghetti.

Install

pip install promptlibretto                # library only
pip install "promptlibretto[ollama]"      # adds httpx for OllamaProvider
pip install "promptlibretto[studio]"      # adds the browser studio
pip install "promptlibretto[dev]"         # pytest + pytest-asyncio

Two paths

1. Tune in the studio, load JSON in your app

pip install "promptlibretto[studio,ollama]"
promptlibretto-studio --port 8000

Open http://localhost:8000. The studio has two pages:

  • Studio (/) — pick selections, set runtime modes, fill template vars, pre-generate, and generate against your local Ollama. Click Export Model JSON to copy the canonical registry.
  • Builder (/builder) — visual form-based editor for constructing a registry from scratch. Hit Load Example to see a fully populated registry covering every section, or Import JSON to load an existing one. Generate Registry exports the JSON; Open in Studio sends it directly to the Studio.

Studio Compose view

Builder overview

When you like the setup, click Export Model JSON in the Studio to copy the canonical registry — including selections, runtime modes, slider positions, and generation overrides. Then in your app:

import asyncio
from promptlibretto import load_registry, OllamaProvider

eng = load_registry(
    "twitch_chatter.json",
    provider=OllamaProvider("http://localhost:11434", "/api/chat"),
)

async def main():
    result = await eng.run(state={
        "selections": {
            "sentiment": "positive",
            "personas": "the_troll",
        },
        "array_modes": {
            "sentiment": {"nudges": "random:1", "examples": "none"},
            "examples":  {"items": "random:3"},
        },
        "section_random": {"personas": True},          # re-roll persona each run
        "sliders":        {"sentiment": 8},            # sentiment.scale token
        "template_vars":  {"base_context::location": "the kitchen"},
    })
    print(result.text)

asyncio.run(main())

load_registry() accepts a path, a JSON string, or a dict. It returns an Engine you can call hydrate() on (no LLM, returns the prompt string) or run() on (hydrate + provider call + output policy).

2. Build it in code

import asyncio
from promptlibretto import Engine, MockProvider, Registry

reg = Registry.from_dict({
    "registry": {
        "assembly_order": ["output_prompt_directions", "personas.context"],
        "output_prompt_directions": {
            "required": True,
            "items": [{"name": "rules", "text": "Be brief."}],
        },
        "personas": {
            "required": True,
            "items": [
                {"id": "shy",   "context": "You're nervous about speaking."},
                {"id": "loud",  "context": "You're full of energy."},
            ],
        },
    }
})

eng = Engine(reg, provider=MockProvider())
print(eng.hydrate(state={"selections": {"personas": "loud"}}))
# Be brief.
#
# You're full of energy.

The full state shape (all fields optional):

state = {
    "selections":     {section_key: id | [id, ...]},   # required: id, multi: [ids]
    "array_modes":    {section_key: {field: "all" | "none" | "index:N" | "random:K"}},
    "section_random": {section_key: bool},             # re-roll item at hydrate time
    "sliders":        {section_key: int},              # drives `<section>.scale` token
    "slider_random":  {section_key: bool},             # re-roll slider at hydrate time
    "template_vars":  {"<section>::<var>": "value"},   # filled into {var} placeholders
}

Optional routes swap assembly_order / generation / output_policy per call:

reg.routes["short"] = Route(assembly_order=["output_prompt_directions"])
result = await eng.run(state=..., route="short")

If a registry has no routes, the top-level assembly_order is the only path.

Example registry shape

This is a neutral shape example, not default content. Keep only the sections and tokens your prompt actually needs.

{
  "registry": {
    "version": 22,
    "title": "Example Registry",
    "assembly_order": [
      "output_prompt_directions",
      "base_context.text",
      "personas.context",
      "sentiment.context",
      "sentiment.nudges",
      "sentiment.scale",
      "examples.normal_examples",
      "sentiment.examples",
      "prompt_endings"
    ],
    "base_context": {
      "required": true,
      "template_vars": ["location"],
      "items": [{
        "name": "context",
        "text": "Use this runtime context: {location}."
      }]
    },
    "personas": { "required": true, "items": [/* {id, context, base_directives[]} */] },
    "sentiment": { "required": true, "items": [/* {id, context, nudges[], examples[]} */] },
    "static_injections":   { "required": false, "items": [/* {name, text} */] },
    "runtime_injections":  { "required": false, "items": [/*
      {id, text, include_sections[], required}
    */] },
    "output_prompt_directions": { "required": true, "items": [/* {name, text} */] },
    "examples":            { "required": false, "items": [/*
      {name, items[], pre_context}
    */] },
    "prompt_endings":      { "required": true, "items": [/* {name, items[]} */] }
  }
}

Sections you don't need can be omitted entirely; the registry is open.

Why bother

  • One mental model — sections of items, an assembly order, and per-array runtime modes. No separate route/overlay/injection vocabularies.
  • The studio's JSON is the same JSON the library loads. No codegen, no schema drift between editor and runtime.
  • Conditional text fragments ({if_var, text}) drop cleanly when their variable is empty, so an unfilled {sublocation} doesn't leave a broken sentence.
  • Output policy (length caps, forbidden patterns, required regex, prefix/suffix stripping) and retries on rejection are still here, just nested inside the registry or a route.

Don't use it if you send exactly one prompt shape with one fixed wording. An f-string is fine.

Library API at a glance

Everything below is exported from the top-level package (from promptlibretto import …).

Name What it is
Engine Hydrate / run / stream against a registry.
Registry / Section / Route The model dataclasses. Registry.from_dict / to_dict round-trip the JSON.
HydrateState Per-call state object. HydrateState.from_dict({...}) accepts the dict shown earlier.
hydrate(reg, state, *, route=None, seed=None) Functional version of Engine.hydrate. Useful when you don't need a provider.
load_registry(source, provider) Parse path / JSON string / dict → Engine.
export_json(engine_or_registry) Serialize back to JSON string.
GenerationResult text, accepted, prompt, route, reason, usage, timing, raw
GenerationChunk delta, done, result — yielded by Engine.stream.
GenerationConfig model, temperature, top_p, top_k, max_tokens, repeat_penalty, retries, max_prompt_chars, timeout_ms. max_prompt_chars is stored in config but not enforced by the current engine.
OutputPolicy / OutputProcessor Output cleaning + validation rules, see below.
MockProvider(responder=None, latency_ms=5.0) Echoes the prompt back, optionally transformed. Used in tests.
OllamaProvider Talks to Ollama or any OpenAI-compatible endpoint.
ProviderAdapter (Protocol) Implement async def generate(request) -> ProviderResponse to add your own.
StreamingProviderAdapter Plus def stream(request) -> AsyncIterator[ProviderStreamChunk].
supports_streaming(provider) True iff the provider has a stream method.
SCHEMA_VERSION / SECTION_KEYS The current registry version (22) and canonical section keys.

Streaming

async for chunk in eng.stream(state=...):
    if chunk.delta:
        print(chunk.delta, end="", flush=True)
    if chunk.done:
        result: GenerationResult = chunk.result
        print(f"\n[ok={result.accepted} {result.timing}]")

Engine.stream() raises if the provider doesn't implement StreamingProviderAdapter. OllamaProvider does; MockProvider does too.

Custom provider

from promptlibretto import ProviderAdapter, ProviderRequest, ProviderResponse

class MyProvider:
    async def generate(self, request: ProviderRequest) -> ProviderResponse:
        return ProviderResponse(text="…")  # call your API here

eng = Engine(reg, provider=MyProvider())

OllamaProvider configuration

from promptlibretto import OllamaProvider

OllamaProvider(
    base_url="http://localhost:11434",     # default
    chat_path="/api/chat",                 # or "/v1/chat/completions" for OpenAI-compat
    payload_shape="auto",                  # "ollama" | "openai" | "auto"
    client=httpx.AsyncClient(timeout=120.0),  # optional — pass a custom httpx.AsyncClient
)

payload_shape="auto" picks openai when chat_path contains /v1/, ollama otherwise — covers both real Ollama and OpenAI-compatible shims (LM Studio, llama.cpp's server, vLLM).

OutputPolicy fields

All optional. Registry-level policy applies by default; when a route defines output_policy, the route's fields replace fields with the same names before the processor is built:

OutputPolicy(
    min_length=None, max_length=None,
    strip_prefixes=(),               # remove these prefixes (case-insensitive)
    strip_patterns=(),               # regex; multiline; replaced with ""
    forbidden_substrings=(),         # exact substrings; reject if present
    forbidden_patterns=(),           # regex; reject if matched
    require_patterns=(),             # regex; reject if not matched
    append_suffix=None,              # always appended after cleaning
    collapse_whitespace=True,
)

OutputPolicy.merged_with() itself is additive for sequence-typed fields (strip_prefixes, strip_patterns, forbidden_*, require_patterns) and replace-on-merge for scalars. Engine.run() currently applies route policy with a dictionary update first, so a route-level sequence field replaces the registry-level value for that field rather than extending it.

Generation overrides at registry / route level

reg = Registry.from_dict({
    "registry": {
        "generation": {"temperature": 0.4, "max_tokens": 96},
        "output_policy": {"max_length": 280, "forbidden_substrings": ['"']},
        "routes": {
            "raid": {
                "assembly_order": [...],
                "generation": {"max_tokens": 64},   # overrides for this route
                "output_policy": {"strip_prefixes": ["Sure,"]},
            },
        },
        # … sections …
    }
})

CLI

promptlibretto-studio --host 0.0.0.0 --port 8000

Environment variables (server-side /api/registry/generate only):

Var Default Effect
PROMPT_ENGINE_MOCK 0 1 / true — use MockProvider.
OLLAMA_URL http://localhost:11434 Base URL for Ollama.
OLLAMA_CHAT_PATH /api/chat Chat endpoint path (auto-detects shape).

The studio frontend itself goes browser-direct to the user's local Ollama after asking the server to hydrate the prompt. In that path, response validation/retries are not applied by the Python engine; these vars and the engine's output policy only affect the optional server-side generate route.

Development

pip install "promptlibretto[dev]"
pytest

License

MIT (see LICENSE when added).

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

promptlibretto-0.5.2.tar.gz (90.7 kB view details)

Uploaded Source

Built Distribution

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

promptlibretto-0.5.2-py3-none-any.whl (101.9 kB view details)

Uploaded Python 3

File details

Details for the file promptlibretto-0.5.2.tar.gz.

File metadata

  • Download URL: promptlibretto-0.5.2.tar.gz
  • Upload date:
  • Size: 90.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for promptlibretto-0.5.2.tar.gz
Algorithm Hash digest
SHA256 a273669b01e36b6c3246420e999dc0662a07262e605a10f543293ef50a715a57
MD5 1b8d4bb4fc606e1a532a1b49df137246
BLAKE2b-256 3e7e4f6d81e8cc60f8bb7065457233cc65137a8cadcafbdd533da5b0977772f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptlibretto-0.5.2.tar.gz:

Publisher: workflow.yml on sockheadrps/promptlibretto

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file promptlibretto-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: promptlibretto-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 101.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for promptlibretto-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9f51bfdbe03f510f7cec04a6df041aba7f40b53e799b49cec1f7274d8859c756
MD5 281c721786f8da1b8ec1d6b4efcb7441
BLAKE2b-256 bf63c46053a4d4fdbeea7045eadfd6a28912cea73e13a411acfc119ac9360a4a

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptlibretto-0.5.2-py3-none-any.whl:

Publisher: workflow.yml on sockheadrps/promptlibretto

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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