Composable prompt-construction library with named routes, context overlays, injections, streaming, middleware, and output validation.
Project description
promptlibretto
A prompt-engineering library — plus a browser studio to design, tune, and export that setup as runnable Python.
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.
Studio (browser designer): studio/.
Skip the prompt-chain code — use the studio
If you don't want to wire CompositeBuilder / PromptRoute / overlays
by hand, design your setup in the browser, export it as a .py, and
import it.
End-to-end in five steps
1. Install the studio and a model provider (Ollama shown — any provider works):
pip install "promptlibretto[studio,ollama]"
2. From your project root, launch the studio. Saves land in ./promptlibretto_exports/ by default — PROMPTLIBRETTO_EXPORT_DIR=. drops them in CWD instead:
PROMPTLIBRETTO_EXPORT_DIR=. promptlibretto-studio
3. In the browser: pick a route, edit the base context, add overlays. For each overlay you want filled at call time, set its runtime dropdown to optional or required. Generate, iterate, inspect the debug trace until you're happy.
4. Click Export as Python → type a name (e.g. my_assistant) → Save to disk. You now have ./my_assistant.py next to your app code.
5. In your app, depend on the library at runtime (no [studio] extra needed) and import the export:
pip install "promptlibretto[ollama]"
# your_app.py
import asyncio
from my_assistant import run
async def main():
# Required + optional runtime slots are keyword args on run().
# Anything else (**extra) becomes a priority-10 overlay for that call.
result = await run(
"what should I cook tonight?",
location="kitchen", # required slot you marked in the studio
focus="quick weeknight meal", # optional slot
dietary="vegetarian", # ad-hoc context via **extra
)
print(result.text)
asyncio.run(main())
That's the whole integration. The exported file reconstructs a
PromptEngine with the exact config, base context, fixed overlays, and
resolved route sections you tuned in the studio — your app depends on
the promptlibretto library at runtime, not on the studio.
Want to tweak it later? Click Load scenario on the saved-export entry to restore the exact studio state that produced it, edit, and re-export over the same file.
The walkthrough below covers each studio panel in detail. If you'd rather build the engine in code, jump to Minimal example.
Recommended workflow
pip install "promptlibretto[studio]"and launchpromptlibretto-studio.- In the browser: pick a route, edit the base context, add overlays, toggle injections, tweak overrides. Generate, iterate, inspect the debug trace.
- Need to try a one-off tweak without touching the route? Click Edit prompt next to the route selector to override the resolved system / user text for the next run(s). The override sticks until you clear it; it's never written back to the route.
- Click Export as Python → Copy the snippet, Download it as
a
.pyfile, or Save to disk under a name. Saves land in./promptlibretto_exports/by default — runpromptlibretto-studiofrom your project root and the file appears right in the project. Override the target withPROMPTLIBRETTO_EXPORT_DIR=./src/prompts.
The exported snippet reconstructs a PromptEngine with the exact config,
base context, overlays, and resolved route sections you tuned. Dynamic
user-input slots survive as lambdas. Your app imports the library and
runs it — no studio dependency at runtime.
promptlibretto.export_python(engine, route="...") is also a public API —
generate the same snippet from any PromptEngine you've wired up in
code, not just one built interactively. GenerationRequest.section_overrides
is the library-level equivalent of the Edit prompt button: pass
{"system": "...", "user": "..."} to bypass the builder for one call.
What the exported file looks like
Every export ships with an async def run(user_input="", *, <slots>, **extra)
wrapper, so calling code doesn't need to know about overlays:
# my_assistant.py — generated by promptlibretto.export_python
from promptlibretto import PromptEngine, ContextOverlay
engine = PromptEngine(...) # config, base, fixed overlays, route
async def run(user_input="", *, location, focus="", **extra):
"""Required runtime slots: location. Optional: focus.
Extra kwargs become priority-10 overlays."""
if not location:
raise ValueError("runtime slot 'location' is required")
engine.context_store.set_overlay("location", ContextOverlay(text=location, priority=20))
if focus:
engine.context_store.set_overlay("focus", ContextOverlay(text=focus, priority=15))
for _name, _value in extra.items():
if _value:
engine.context_store.set_overlay(_name, ContextOverlay(text=str(_value), priority=10))
return await engine.generate_once(user_input)
from my_assistant import run
result = await run("what should I do?", location="kitchen", focus="cleanup")
In the studio, each overlay card has a runtime dropdown:
- fixed — text is baked into the export.
- runtime — optional — becomes a keyword arg with a
""default; only applied if non-empty. - runtime — required — becomes a required keyword arg; raises
ValueErrorif empty.
Runtime-tagged overlays are skipped during studio generation (their text is a placeholder, not a value), and the exported run() clears any prior runtime/**extra overlays at entry so calls don't leak state into one another.
Editing an export later
When you Save to disk, the studio also snapshots the current state as a scenario under the same name. The saved-exports list shows a Load scenario button — click it to restore the exact studio setup that produced the export, edit, and re-export. No round-trip parser; the scenario is the editable form.
Install
pip install promptlibretto # library only, no runtime deps
pip install "promptlibretto[ollama]" # adds httpx for OllamaProvider
pip install "promptlibretto[studio]" # adds FastAPI stack for the browser studio
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 PromptEngine
engine = PromptEngine(routes={"default": "Say hi."})
print(asyncio.run(engine.generate_once()).text)
Everything after this adds features on top of the same shape. The constructor accepts looser types than the named classes suggest:
config→GenerationConfig,dict, or omitted (defaults to mock).context_store→ContextStore,str(base text),dict, or omitted.provider→ProviderAdapter,"mock","ollama", or omitted.routes→{name: str | list | dict | CompositeBuilder | PromptRoute}— strings and lists become user sections automatically.generate_oncetakes aGenerationRequest, adict, a bare string (wrapped intoinputs={"input": ...}), or nothing.
Drop the full classes in when you want the full surface — no special paths.
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.acceptedisFalse, fall back togenerate_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_overrideswin over a route'sgeneration_overrides, which win over the engine's baseGenerationConfig. The trace exposes all four layers undermetadata.config_layersso 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
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 promptlibretto-0.3.1.tar.gz.
File metadata
- Download URL: promptlibretto-0.3.1.tar.gz
- Upload date:
- Size: 76.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce3820c6a854b6a565bc58a9cd723bee7ee5fb769dfb5756bda7c627ed36bad8
|
|
| MD5 |
ecc4244224ca7ae24c93d9bf529c5168
|
|
| BLAKE2b-256 |
5ef9d4f79368fb080ebd7340411e15f9649f51ae25ae054152a1d91b261a5f74
|
Provenance
The following attestation bundles were made for promptlibretto-0.3.1.tar.gz:
Publisher:
workflow.yml on sockheadrps/promptlibretto
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptlibretto-0.3.1.tar.gz -
Subject digest:
ce3820c6a854b6a565bc58a9cd723bee7ee5fb769dfb5756bda7c627ed36bad8 - Sigstore transparency entry: 1346243441
- Sigstore integration time:
-
Permalink:
sockheadrps/promptlibretto@82921a6041b148e3edd2bb90017770b504acec25 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/sockheadrps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@82921a6041b148e3edd2bb90017770b504acec25 -
Trigger Event:
push
-
Statement type:
File details
Details for the file promptlibretto-0.3.1-py3-none-any.whl.
File metadata
- Download URL: promptlibretto-0.3.1-py3-none-any.whl
- Upload date:
- Size: 84.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04ac977cd9d475b35d6e3abfcfcfcbae136f0ab98aa51bb63498c42117c83816
|
|
| MD5 |
98841331850d0e8524650ece81ed5c81
|
|
| BLAKE2b-256 |
b4d9f18d4b65cd7c193a4c840565266047b497415b1269b48d1504b40157db35
|
Provenance
The following attestation bundles were made for promptlibretto-0.3.1-py3-none-any.whl:
Publisher:
workflow.yml on sockheadrps/promptlibretto
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptlibretto-0.3.1-py3-none-any.whl -
Subject digest:
04ac977cd9d475b35d6e3abfcfcfcbae136f0ab98aa51bb63498c42117c83816 - Sigstore transparency entry: 1346243468
- Sigstore integration time:
-
Permalink:
sockheadrps/promptlibretto@82921a6041b148e3edd2bb90017770b504acec25 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/sockheadrps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@82921a6041b148e3edd2bb90017770b504acec25 -
Trigger Event:
push
-
Statement type: