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.
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.
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 PartsA 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 untestedPrompts 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.
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).
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.
- 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/).
Requires Python 3.11+. Dual-licensed: MIT or Apache-2.0, at your option.
pip install peisinoe # ALL of Peisinoe (core, evals, storage) — zero dependencies
pip install "peisinoe[storage]" # same code + PyYAML, the loader's default parser
# (or skip the extra and pass your own with load(..., parse=...))For development, from a checkout:
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,storage]"
pytest
ruff check . && mypy peisinoe_core peisinoe_toolsKnowledge 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.
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).