Deterministic, content-addressed prompt composition — the Peisinoe core (Python reference implementation)
Project description
Peisinoe
Prompt composition with explicit variants and deterministic identity.
Peisinoe is a Python library for building prompts from reusable, typed parts — text and binary alike. Parts can be combined, selected, and ordered declaratively, so feature flags, A/B variants, and other prompt experiments remain part of the prompt definition rather than being scattered through application code.
Prompts can be authored in Markdown and YAML packages and loaded by name, or constructed directly in Python. Resolving a prompt binds its inputs and produces model-ready messages together with deterministic identities for the selected structure and the exact filled instance. Recorded alongside a model's output, they identify exactly which composition produced it.
The eval layer uses the same structure to enumerate reachable variants, target
them, and report coverage. Model execution stays in the application through a
small bring-your-own Model interface; the core has zero runtime dependencies.
Why it exists
Prompt iteration often changes both content and the logic that selects or combines it. When that content and logic live inside application code, experiments can require edits across unrelated code paths, variants are harder to exercise systematically, and the exact prompt used for a result may be difficult to reconstruct.
Peisinoe keeps prompt source and composition separate from application flow. Variants remain explicit, reusable parts can be shared, and each resolution carries a content-derived identity.
A taste
Author prompts as files and load them by name — application code loads a package once, then addresses assemblies and units by name:
support.prompt/ # a package (a ".prompt" folder)
├── triage.assembly.yaml # an assembly: named parts wired to units
├── system/unit.yaml # a unit: params + sections (Select, Child, …)
└── user_message.md # a unit: a bare Markdown file *is* a unit
from peisinoe_tools.storage import load
pkg = load("support.prompt") # point at the folder once
prompt = pkg["triage"] # get the assembly by name
prompt.resolve({"tier": "pro", "question": "…"}).materialize()
The same structures can be constructed directly in Python:
import peisinoe_core as p
support = p.Unit("support", params=("tier",), sections=(
p.Static("hi", "Hello!"),
p.Select("policy", on="tier", cases={
"free": p.Static("f", "Basic help."),
"pro": p.Static("pp", "Priority help.", tags=("safety",)),
}, default="free"),
))
r = support.resolve({"tier": "pro"})
r.structure_hash # the variant (values excluded) — your attribution key
r.materialize() # typed, role-tagged Messages of typed Parts
A loaded unit compiles to the same identity as the hand-built one
(loaded.full_version() == built.full_version()) — the loader is a compiler.
Target an eval at a variant, then see what you missed:
from peisinoe_tools.evals import EvalSpec, Target, Contains, plan, coverage
spec = EvalSpec("mentions_help", Target(all_of=("safety",)), Contains("help"))
pl = plan(support, [spec])
coverage(pl).branches_uncovered # ('support > policy=free',) — the free branch is untested
Beyond text
Prompts aren't only text. A single Markdown file can define a complete multimodal prompt — a scalar hole and a typed binary hole:
---
params:
instruction:
image:
type: blob
---
{{instruction}}
{{image}}
edit = load("image_edit.prompt").unit("edit")
r = edit.resolve({"instruction": "turn the cat into a tiger",
"image": p.Blob("image/png", png_bytes)})
A binary hole also takes an ordered list ([img_a, img_b, img_c] — a
model's "up to N reference images" is one param), and 1-vs-N values is the
same structure_hash: count, order, and content are instance data.
Design model
Peisinoe uses the following model:
- Prompt as a program, not a string — it branches, and the branch space is enumerable.
- Identity is content-addressed; names and versions are addressing —
identity is derived from the canonical template or resolved representation,
not assigned by a label. A
version:label is a mutable human pointer (like a git tag); hashes are computed, never written into files. - Binary is prompt content, not an attachment — images, PDFs, and external
refs fill typed holes and participate in prompt identity and redaction;
Refvalues also support deferred loading. A runnable image-editing integration is included inexamples/integrations/. - Target by intent, not position — evals point at tags (what a block is about), not block names (where it is).
- You ask "what did I cover?", not just "did it pass?" — because branches are explicit, coverage can report which cases an eval plan exercised.
- Record, don't control — the eval layer scores what a bring-your-own model returns; it never drives the model (so it works unchanged with multimodal output, agents, even diffusion models).
When (not) to use it
Use it when you have prompt variation worth tracking — variants, tiers, A/B flags, evals you want to target and cover, or a need to attribute results to an exact prompt.
It won't ship a provider adapter, route to providers, stream, or score by
token log-probabilities (a deliberate boundary — it scores observed output, not
model internals). You bring the Model; the eval layer calls it and scores what
it returns.
Documentation
- Tutorial — the whole loop end to end in ~15 minutes: build a branching prompt, see its identity, author it on disk, target evals, and find the branch you missed.
- Core — composition and the identity model (typed parts,
structure_hash/instance_hash, branching, redaction). - Storage — author prompts as
.promptfolders and load them by name (unit.yaml/*.assembly.yaml, params, binary holes, versions, imports). - Evals — targeting by tags, coverage, datasets, scoring.
- examples/ — runnable scripts: zero-setup library demos
(
core/,evals/) and real-model integrations (integrations/).
Install (development)
Requires Python 3.11+. Dual-licensed: MIT or Apache-2.0, at your option. Not yet on PyPI — install from a checkout:
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,storage]" # ".[storage]" adds the loader's YAML parser (or bring your own)
pytest
ruff check . && mypy peisinoe_core peisinoe_tools
Knowledge flows downward only: peisinoe_core (zero-dependency composition +
identity), peisinoe_tools.evals (the eval layer built on it), and
peisinoe_tools.storage (an opt-in loader that compiles .prompt folders to the
same core objects). The core stays dependency-free; only storage has an optional
extra.
The name
Peisinoë is one of the Sirens — the singers whose composed voices drew sailors in. A fitting namesake for a library about composing prompts (and, with luck, a little luring). 🙂
Alpha / reference implementation. The hashing is locked by golden vectors
(testvectors/core.json) for cross-language conformance — regenerate after an
intentional hashing change and bump HASHSPEC
(python -m tests.gen_vectors).
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 peisinoe-0.1.0.tar.gz.
File metadata
- Download URL: peisinoe-0.1.0.tar.gz
- Upload date:
- Size: 181.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7647c23454e2c59c09f0e3b51d3aeb2527e705ec4e77dcd73ab6b7f78a73d88a
|
|
| MD5 |
896b92b9c28a4affd079f9d621f0e957
|
|
| BLAKE2b-256 |
4fe0b8bab3be8f8256f5f951bdc8d0a346dead82b574ce05e0a0cc3d5080695d
|
File details
Details for the file peisinoe-0.1.0-py3-none-any.whl.
File metadata
- Download URL: peisinoe-0.1.0-py3-none-any.whl
- Upload date:
- Size: 105.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1dae4402e05b3c4e1fe0d551d1cd9a682a6dddab75c17ec16edcf37c6f84e70d
|
|
| MD5 |
d49abfdabcfb896dc3d2efbb069a5dcf
|
|
| BLAKE2b-256 |
973fa9a80ed9788cddcc3aad1e971f03ef6b20ed36c2b6c8ed145e276d608cd8
|