Deriving a prompt
Prompt is immutable — there are no setters. derive is the single, general-purpose way to
produce a changed prompt: it shallow-replaces any subset of top-level fields, re-validates the
merged whole (agreement, parse, reserved-variant-name), and returns a new Prompt — the
original is untouched. The method is spelled derive in all three languages.
It is general — use it to replace the body, rename, swap the variables map, adjust metadata, or add
a variant at runtime. (To declare alternative bodies up front in the prompt
document, that’s simpler — see Variants. This page is for
deriving a changed copy of an already-constructed Prompt.)
Two complementary surfaces work together:
- Read the current fields with the accessors —
.variants()(Rust) /.variants(Python & TypeScript properties),.body()/.name(), etc. These never mutate; they return what the prompt currently holds. - Derive a changed copy with the sole mutator
derive..variants()andderiveare not alternatives — the accessor cannot change anything, andderiveis the only thing that can; the “add a variant” example below uses both together (read with.variants(), write withderive).
What can be overlaid
Section titled “What can be overlaid”Any top-level field of the prompt definition:
| Field | Overlay type |
|---|---|
name | string |
role | "system" | "user" | "assistant" |
body | string (template source) |
variables | full variables map (replaces the entire map) |
variants | full variants map (replaces the entire map) |
output_model | string | null (TS/Py) / Option<String> (Rust) |
metadata | opaque object |
Fields absent from the overlay are kept from the original. Re-validation runs over the merged whole — so an overlay that introduces an agreement violation (a new body that references an undeclared variable) is rejected.
variants is replaced wholesale — read, then spread
Section titled “variants is replaced wholesale — read, then spread”This is the one sharp edge: derive does a shallow replace per top-level field, so an overlay’s
variants map replaces the entire existing map — it does not merge. When the prompt already has
variants and the intent is to add one while keeping the rest, read the current map with
.variants() and spread it into the overlay. (When the prompt has no variants yet, pass the
new map directly — there is nothing to preserve.)
.variants() is a read accessor (it returns the current map; it never mutates). derive is the
only mutator. The pattern below uses both together: read with .variants(), write with derive.
The examples on this page start from an assistant prompt (a company + max_words body) and a matching
AssistantVars — the same pair from Getting started:
//! Derive guide — the starting pair: an `assistant` system prompt (a `company` +//! `max_words` body) and a matching `AssistantVars`. Every later example on the page//! derives from this. Standalone — `cargo run --example guides_derive_setup`.
use garde::Validate;use prompting_press::Prompt;use serde::Serialize;use std::fs;
#[derive(Serialize, Validate)]struct AssistantVars { #[garde(length(min = 1))] company: String, #[garde(range(min = 1))] max_words: i64,}
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
// The pair parses and validates: the body's {{ company }}/{{ max_words }} agree // with AssistantVars. let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?; assert_eq!(assistant.name(), "assistant");
// AssistantVars is a plain garde-validated struct — construct one to prove the shape. let vars = AssistantVars { company: "Acme Robotics".into(), max_words: 50, }; assert_eq!(vars.company, "Acme Robotics"); assert_eq!(vars.max_words, 50); Ok(())}"""Derive guide — the starting pair: an ``assistant`` prompt (a ``company`` + ``max_words``system body) and a matching ``AssistantVars``. Every later example on the page derives fromthis.
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import Promptfrom pydantic import BaseModel, field_validator
# The caller reads the definition; the library does no file I/O itself.# Resolve the file next to this program (a real app uses its own path)._HERE = Path(__file__).parent
class AssistantVars(BaseModel): company: str max_words: int
@field_validator("max_words") @classmethod def _at_least_one(cls, v: int) -> int: if v < 1: raise ValueError("max_words must be at least 1") return v
def main() -> None: # The pair parses and validates: the body's {{ company }}/{{ max_words }} agree with # AssistantVars. assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text()) assert assistant.name == "assistant"
# AssistantVars is a plain Pydantic model — construct one to prove the shape. vars = AssistantVars(company="Acme Robotics", max_words=50) assert vars.company == "Acme Robotics" assert vars.max_words == 50
if __name__ == "__main__": main()/** * Derive guide — the starting pair: an `assistant` prompt (a `company` + `max_words` system * body) and a matching `AssistantVars`. Every later example on the page derives from this. * * Standalone — the docs page displays this file verbatim; run it directly to check. */
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
test("assistant + AssistantVars is the starting pair", () => { // The pair parses and validates: the body's {{ company }}/{{ max_words }} agree with AssistantVars. const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8")); assert.equal(assistant.name, "assistant");
// AssistantVars is a plain Zod schema — parse a value to prove the shape. const vars = AssistantVars.parse({ company: "Acme Robotics", max_words: 50 }); assert.equal(vars.company, "Acme Robotics"); assert.equal(vars.max_words, 50);});//! Derive guide — add a variant at runtime: READ the current variants with the//! `.variants()` accessor, add to a clone, then WRITE the merged map back via the sole//! mutator `derive`. The original is untouched.//! Standalone — `cargo run --example guides_derive_add_variant`.
use prompting_press::{Prompt, PromptOverlay};use serde_json::json;use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples"); let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// READ the current variants, then add to a clone — so existing arms survive. let mut variants = assistant.variants().clone(); variants.insert( "formal".to_string(), serde_json::from_value(json!({ "body": "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words." }))?, );
// WRITE the merged map back via the sole mutator. let formal_assistant = assistant.derive(PromptOverlay { variants: Some(variants), ..Default::default() })?; // assistant is unchanged; formal_assistant is a new, fully-validated Prompt.
assert!(assistant.variants().is_empty(), "original is untouched"); assert!(formal_assistant.variants().contains_key("formal")); Ok(())}"""Derive guide — add a variant at runtime: spread the current ``variants`` map (read viathe accessor) into the overlay, then add one — so existing arms survive. ``derive`` is thesole mutator; the original is untouched.
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import Prompt
# The caller reads the definition; the library does no file I/O itself.# Resolve the file next to this program (a real app uses its own path)._HERE = Path(__file__).parent
def main() -> None: assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
# READ the current variants (spread), then add one — so existing arms survive. derived = assistant.derive( { "variants": { **assistant.variants, # keep what's already there "formal": { "body": "You are the official support assistant for {{ company }}. " "Please keep every reply under {{ max_words }} words." }, } } ) # assistant is unchanged; derived is a new, fully-validated Prompt.
assert dict(assistant.variants) == {}, "original is untouched" assert "formal" in derived.variants
if __name__ == "__main__": main()derive(overlay, *, validators=None) — overlay is a plain dict. assistant.variants is the read-only
accessor for the current map.
/** * Derive guide — add a variant at runtime: spread the current `variants` map (read via the * accessor) into the overlay, then add one — so existing arms survive. `derive` is the sole * mutator; the original is untouched. * * Standalone — the docs page displays this file verbatim; run it directly to check. */
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
test("derive adds a variant, leaving the original untouched", () => { const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
// READ the current variants (spread), then add one — so existing arms survive. const derivedAssistant = assistant.derive({ variants: { ...assistant.variants, // keep what's already there formal: { body: "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words.", }, }, }); // assistant is unchanged; derivedAssistant is a new, fully-validated Prompt.
assert.deepEqual(assistant.variants ?? {}, {}, "original is untouched"); assert.ok("formal" in (derivedAssistant.variants ?? {}));});derive(overlay, validators?) — overlay is a partial PromptDefinition object. assistant.variants is
the read-only accessor for the current map.
Replacing the body
Section titled “Replacing the body”Replacing only the root body (the default arm):
//! Derive guide — replace only the root body (the default arm) with `derive`.//! Standalone — `cargo run --example guides_derive_replace_body`.
use garde::Validate;use prompting_press::{GuardConfig, Prompt, PromptOverlay};use serde::Serialize;use std::fs;
#[derive(Serialize, Validate)]struct AssistantVars { #[garde(length(min = 1))] company: String, #[garde(range(min = 1))] max_words: i64,}
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples"); let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let brief_assistant = assistant.derive(PromptOverlay { body: Some("You are a support assistant for {{ company }}.".to_string()), ..Default::default() })?;
let vars = AssistantVars { company: "Acme Robotics".into(), max_words: 50, }; let result = brief_assistant.render(&vars, None, &GuardConfig::default(), false)?; assert_eq!( result.text, "You are a support assistant for Acme Robotics." ); Ok(())}"""Derive guide — replace only the root body (the default arm) with ``derive``.
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import Promptfrom pydantic import BaseModel, field_validator
# The caller reads the definition; the library does no file I/O itself.# Resolve the file next to this program (a real app uses its own path)._HERE = Path(__file__).parent
class AssistantVars(BaseModel): company: str max_words: int
@field_validator("max_words") @classmethod def _at_least_one(cls, v: int) -> int: if v < 1: raise ValueError("max_words must be at least 1") return v
def main() -> None: assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
brief_assistant = assistant.derive( {"body": "You are a support assistant for {{ company }}."} )
result = brief_assistant.render( AssistantVars, data={"company": "Acme Robotics", "max_words": 50} ) assert result.text == "You are a support assistant for Acme Robotics."
if __name__ == "__main__": main()/** * Derive guide — replace only the root body (the default arm) with `derive`. * * Standalone — the docs page displays this file verbatim; run it directly to check. */
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
test("derive replaces only the root body", () => { const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
const briefAssistant = assistant.derive({ body: "You are a support assistant for {{ company }}." });
const result = briefAssistant.render(AssistantVars, { company: "Acme Robotics", max_words: 50 }); assert.equal(result.text, "You are a support assistant for Acme Robotics.");});Re-validation on overlay
Section titled “Re-validation on overlay”If the merged definition violates any construction invariant, derive returns an error
(Rust Result::Err) or raises/throws. Example — overlaying a body that references an undeclared
variable:
//! Derive guide — re-validation on overlay: overlaying a body that references an//! undeclared variable is rejected over the merged whole (agreement failure).//! Standalone — `cargo run --example guides_derive_revalidation_error`.
use prompting_press::{ConsumerError, Prompt, PromptOverlay};use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples"); let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let bad = assistant.derive(PromptOverlay { body: Some("You help {{ ghost }}.".to_string()), ..Default::default() }); match bad { Err(ConsumerError::Kernel(rows)) => { assert_eq!(rows[0].code, "undefined_variable"); assert_eq!(rows[0].field, "ghost"); } _ => unreachable!("the merged definition is agreement-unsound"), } Ok(())}"""Derive guide — re-validation on overlay: overlaying a body that references anundeclared variable raises ``PromptRenderError`` (agreement failure over the merged whole).
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import Prompt, PromptRenderError
# The caller reads the definition; the library does no file I/O itself.# Resolve the file next to this program (a real app uses its own path)._HERE = Path(__file__).parent
def main() -> None: assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
try: bad = assistant.derive({"body": "You help {{ ghost }}."}) except PromptRenderError as exc: print(exc.errors[0].code) # "undefined_variable" print(exc.errors[0].field) # "ghost" assert exc.errors[0].code == "undefined_variable" assert exc.errors[0].field == "ghost" else: raise AssertionError(f"expected PromptRenderError, got {bad!r}")
if __name__ == "__main__": main()/** * Derive guide — re-validation on overlay: overlaying a body that references an undeclared * variable throws `PromptRenderError` (agreement failure over the merged whole). * * Standalone — the docs page displays this file verbatim; run it directly to check. */
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt, PromptRenderError } from "prompting-press";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
test("derive re-validates the merged whole and rejects an undeclared variable", () => { const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
try { const bad = assistant.derive({ body: "You help {{ ghost }}." }); throw new Error(`expected PromptRenderError, got ${JSON.stringify(bad)}`); } catch (err) { if (err instanceof PromptRenderError) { console.error(err.errors[0].code); // "undefined_variable" console.error(err.errors[0].field); // "ghost" assert.equal(err.errors[0].code, "undefined_variable"); assert.equal(err.errors[0].field, "ghost"); } else { throw err; } }});Validators carry forward (Python / TypeScript)
Section titled “Validators carry forward (Python / TypeScript)”In Python and TypeScript, the validators supplied at construction carry forward to the derived
Prompt by default. Pass validators=NewModel (Python) or derive(overlay, newSchema)
(TypeScript) to override.
Rendering a named variant
Section titled “Rendering a named variant”After adding a variant, select it by name at render time:
//! Derive guide — render a named variant: after adding a variant with `derive`, select//! it by name at render time. Variant selection is caller-owned.//! Standalone — `cargo run --example guides_derive_render_variant`.
use garde::Validate;use prompting_press::{GuardConfig, Prompt, PromptOverlay};use serde::Serialize;use serde_json::json;use std::fs;
#[derive(Serialize, Validate)]struct AssistantVars { #[garde(length(min = 1))] company: String, #[garde(range(min = 1))] max_words: i64,}
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples"); let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?; let mut variants = assistant.variants().clone(); variants.insert( "formal".to_string(), serde_json::from_value(json!({ "body": "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words." }))?, ); let formal_assistant = assistant.derive(PromptOverlay { variants: Some(variants), ..Default::default() })?;
let vars = AssistantVars { company: "Acme Robotics".into(), max_words: 50, }; let result = formal_assistant.render(&vars, Some("formal"), &GuardConfig::default(), false)?; assert_eq!( result.text, "You are the official support assistant for Acme Robotics. Please keep every reply under 50 words." ); assert_eq!(result.variant, "formal"); Ok(())}"""Derive guide — render a named variant: after adding a variant with ``derive``, selectit by name at render time. Variant selection is caller-owned.
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import Promptfrom pydantic import BaseModel, field_validator
# The caller reads the definition; the library does no file I/O itself.# Resolve the file next to this program (a real app uses its own path)._HERE = Path(__file__).parent
class AssistantVars(BaseModel): company: str max_words: int
@field_validator("max_words") @classmethod def _at_least_one(cls, v: int) -> int: if v < 1: raise ValueError("max_words must be at least 1") return v
def main() -> None: assistant = Prompt.from_yaml((_HERE / "assistant.yaml").read_text()) derived = assistant.derive( { "variants": { **assistant.variants, "formal": { "body": "You are the official support assistant for {{ company }}. " "Please keep every reply under {{ max_words }} words." }, } } )
result = derived.render( AssistantVars, data={"company": "Acme Robotics", "max_words": 50}, variant="formal", ) print( result.text ) # "You are the official support assistant for Acme Robotics. ..." print(result.variant) # "formal" assert ( result.text == "You are the official support assistant for Acme Robotics. " "Please keep every reply under 50 words." ) assert result.variant == "formal"
if __name__ == "__main__": main()/** * Derive guide — render a named variant: after adding a variant with `derive`, select it by * name at render time. Variant selection is caller-owned. * * Standalone — the docs page displays this file verbatim; run it directly to check. */
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
test("a derived named variant renders by name", () => { const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8")); const derivedAssistant = assistant.derive({ variants: { ...assistant.variants, formal: { body: "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words.", }, }, });
const result = derivedAssistant.render( AssistantVars, { company: "Acme Robotics", max_words: 50 }, { variant: "formal" }, ); console.log(result.text); // "You are the official support assistant for Acme Robotics. Please keep every reply under 50 words." console.log(result.variant); // "formal" assert.equal( result.text, "You are the official support assistant for Acme Robotics. Please keep every reply under 50 words.", ); assert.equal(result.variant, "formal");});Variant selection is caller-owned — the library validates the name and renders it. It does not own experiment-assignment logic or choose variants automatically.
docs current as of 0.2.0