Skip to main content

Composable prompt-construction library with named routes, context overlays, injections, streaming, middleware, and output validation.

Project description

promptlibretto

A small library for apps that send more than one kind of prompt to an LLM. Define named routes that each compose their own system + user prompt, sampling params, and output policy. Layer transient context overlays on a long-lived base. Attach stackable injections for cross-cutting style/format tweaks. Swap providers without touching the rest.

Good fit for: multi-mode assistants, agents that switch strategies per task, prompt A/B testing, iterative refinement loops where each user follow-up becomes a reusable overlay, and any app where prompt-construction logic has outgrown f-strings.

Design rationale: DESIGN.md. Browser test bench: testbench/.

Analyst route generating

Install

pip install promptlibretto                # library only, no runtime deps
pip install "promptlibretto[ollama]"      # adds httpx for OllamaProvider
pip install "promptlibretto[testbench]"   # adds FastAPI stack for the test bench
pip install "promptlibretto[dev]"         # adds pytest + pytest-asyncio

Hello world

The smallest useful engine — one route, mock provider, one line of output:

import asyncio
from promptlibretto import (
    CompositeBuilder, ContextStore, GenerationConfig, GenerationRequest,
    MockProvider, OutputProcessor, PromptAssetRegistry, PromptEngine,
    PromptRoute, PromptRouter, section,
)

router = PromptRouter(default_route="default")
router.register(PromptRoute(
    name="default",
    builder=CompositeBuilder(name="default", user_sections=(section("Say hi."),)),
))

engine = PromptEngine(
    config=GenerationConfig(provider="mock", model="demo"),
    context_store=ContextStore(base=""),
    asset_registry=PromptAssetRegistry(),
    router=router,
    provider=MockProvider(),
    output_processor=OutputProcessor(),
)

print(asyncio.run(engine.generate_once(GenerationRequest())).text)

Everything after this adds features on top of the same shape.

Why not just f-strings?

Use this when:

  • You have more than one kind of prompt and they share structure — frame, rules, persona, output format. Routes let you name and swap strategies without duplicating boilerplate.
  • Follow-ups should affect future runs, not just the current one. Overlays let a user's "make it shorter" stick around as a reusable piece of context.
  • Output needs validation or retry — required regex, stripped code fences, banned phrases — handled once by the output processor instead of copy-pasted around call sites.

Don't use it when you send exactly one prompt shape and don't need any of the above. An f-string and a direct provider call are fine.

Core concepts

Piece What it does
GenerationConfig Sampling params + provider/model selection. Immutable; merged_with().
ContextStore Long-lived base + named overlays with priority and optional expiry.
PromptAssetRegistry Named snippets: frames, rules, personas, endings, example/nudge pools, injectors.
PromptRoute / Router Named composition strategies. Router picks one per request.
CompositeBuilder Assembles system + user prompts from ordered section callables.
ProviderAdapter Runs the model. Ships with OllamaProvider, MockProvider.
OutputProcessor Cleans and validates model output against a policy.
RecentOutputMemory Bounded log for near-duplicate detection (Jaccard).
RunHistory Bounded log of full runs for replay.
TemplateRenderer {slot} substitution for parameterised base / overlays.
RandomSource Injectable RNG used by example/nudge pools.
PromptEngine Glues it together. generate_once(request) is the entry point.

Flow

GenerationRequest
      │
      ▼
PromptRouter ──► PromptRoute.builder ──► PromptPackage ──► ProviderAdapter
      ▲                ▲                                         │
      │                │                                         ▼
ContextStore     PromptAssetRegistry                     OutputProcessor
                                                                 │
                                                                 ▼
                                                         GenerationResult

Minimal example

import asyncio
from promptlibretto import (
    CompositeBuilder, ContextStore, GenerationConfig, GenerationRequest,
    MockProvider, OutputProcessor, PromptAssetRegistry, PromptEngine,
    PromptRoute, PromptRouter, section,
)
from promptlibretto.builders.builder import BuildContext


def frame(ctx: BuildContext) -> str:
    return ctx.assets.frame("core")


def user_input(ctx: BuildContext) -> str:
    return f"Question:\n{ctx.request.inputs.get('input', '')}"


assets = PromptAssetRegistry()
assets.add_frame("core", "You are a careful, helpful assistant. Be concise.")

router = PromptRouter(default_route="default")
router.register(PromptRoute(
    name="default",
    builder=CompositeBuilder(
        name="default",
        system_sections=(frame,),
        user_sections=(user_input, section("Respond now.")),
    ),
))

engine = PromptEngine(
    config=GenerationConfig(provider="mock", model="demo"),
    context_store=ContextStore(base="The assistant operates in demo mode."),
    asset_registry=assets,
    router=router,
    provider=MockProvider(),
    output_processor=OutputProcessor(),
)

async def main():
    result = await engine.generate_once(GenerationRequest(
        mode="default",
        inputs={"input": "What is entropy?"},
    ))
    print(result.text)

asyncio.run(main())

To run against a real model, swap the provider and config:

from promptlibretto import OllamaProvider

provider = OllamaProvider(base_url="http://localhost:11434")
config = GenerationConfig(provider="ollama", model="llama3")

Everything else stays the same.


Prompt engineering & routing

Context overlays

ContextStore holds one base string plus named overlays. Higher priority applies first; overlays can expire. Use them for transient facts, user preferences, or iteration follow-ups.

from promptlibretto import ContextOverlay, make_turn_overlay

store.set_overlay("budget", ContextOverlay(text="Keep total under $800.", priority=20))
store.set_overlay("iter_1", make_turn_overlay(
    verbatim="actually please make this shorter",
    compacted="Prefer shorter responses.",
    priority=25,
))

Routes and builders

CompositeBuilder takes ordered section callables. Each receives a BuildContext and returns a string — return "" to omit.

PromptRoute(
    name="analyst",
    builder=CompositeBuilder(
        name="analyst",
        system_sections=(frame_fn, persona_fn),
        user_sections=(user_input_fn, section("Summary / Tradeoffs / Open questions.")),
        generation_overrides={"temperature": 0.6, "max_tokens": 700},
        output_policy={"strip_prefixes": ["```"]},
    ),
)

Injections

Named InjectionTemplates registered on the asset registry. Callers pass their names in GenerationRequest.injections to layer instructions, generation overrides, or output policy on top of a route.

assets.add_injector("json_only", InjectionTemplate(
    instructions="Return ONLY minified JSON.",
    generation_overrides={"temperature": 0.2},
    output_policy={"strip_prefixes": ["```json", "```"]},
))

Templating

TemplateRenderer does {slot} substitution with aliases and whitespace normalisation — useful for parameterised base contexts or overlays.


Execution & integration

Providers

  • OllamaProvider(base_url=...) — local Ollama / OpenAI-compatible server.
  • MockProvider() — echoes the prompt; for tests.

Implement async def generate(request) -> ProviderResponse for your own.

Streaming

Providers may implement stream(request). The engine exposes generate_stream(request) yielding GenerationChunk(delta=...) per chunk and a terminal GenerationChunk(done=True, result=...).

async for chunk in engine.generate_stream(request):
    if chunk.done:
        final = chunk.result
    elif chunk.delta:
        print(chunk.delta, end="", flush=True)

⚠ Warning — Streaming makes exactly one provider call; output-policy retries are skipped. If result.accepted is False, fall back to generate_once.

Output processor

Applies a policy derived from the route + injection overrides: strip code fences, enforce required regex, reject forbidden substrings. Rejected attempts retry up to GenerationConfig.retries times.

Prompt-size budget

Set max_prompt_chars on GenerationConfig to cap the outgoing prompt. When over budget, the engine drops the lowest-priority overlay and rebuilds until it fits. The debug trace reports which overlays were dropped under metadata.budget.


Observability & production

Reproducibility

Pass SeededRandom(n) for deterministic example/nudge picks:

engine = PromptEngine(..., random=SeededRandom(42))

Middleware

Cross-cutting concerns (logging, metrics, caching, redaction) without touching prompt construction. Any object with before(request) and/or after(request, result) — sync or async. Return None to pass through, a new value to replace.

class LatencyLogger:
    async def before(self, request):
        self.started = time.perf_counter()
    async def after(self, request, result):
        print(f"route={result.route} ms={(time.perf_counter() - self.started)*1000:.1f}")

engine = PromptEngine(..., middlewares=[LatencyLogger()])

before runs in registration order, after in reverse. Wraps both generate_once and generate_stream.

Debug trace

GenerationRequest(debug=True) attaches a GenerationTrace with system/user prompts, every attempt, resolved config, and context snapshot.

Note — Config merge order is base → route → request. Values on GenerationRequest.config_overrides win over a route's generation_overrides, which win over the engine's base GenerationConfig. The trace exposes all four layers under metadata.config_layers so you can see exactly who contributed what.

Run history

Plug a RunHistory in and every generate_once is recorded with its request shape for replay. Stores only the caller's explicit config_overrides; resolved config lives in record metadata.


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.1.2.tar.gz (63.5 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.1.2-py3-none-any.whl (71.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for promptlibretto-0.1.2.tar.gz
Algorithm Hash digest
SHA256 57915b0d20acf01de6bdbf8d04807ec5df7c5d63f4b52a8eb875098314cf091a
MD5 acf221baa3b3a47cb4aba1721843b72d
BLAKE2b-256 8f64211062b22721179691b2f0644ad7669430fa568f2a21b3fb6074de93107e

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptlibretto-0.1.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.1.2-py3-none-any.whl.

File metadata

  • Download URL: promptlibretto-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 71.0 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.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cfe35ddf56f79633f32c2be7f2d99ac1d1ec98194dd25f89e279b8c8783277e2
MD5 3d0199030eda15a90c6c4c1088563d6a
BLAKE2b-256 7c3f608518e74f011b4eca76ea1b4b03eb698dc21e804f189ef535d4320225c0

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptlibretto-0.1.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