Deriving a prompt
Prompt is immutable — there are no setters. derive is the single, general-purpose way to
produce a changed prompt: it combines an overlay with the current definition using a
merge strategy, 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, add
a variant at runtime, or union variables from a base into a child. (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.
Merge strategies
Section titled “Merge strategies”derive supports two strategies, selected per-call (default is Replace):
| Strategy | Map fields (variables, variants, metadata) | Scalar fields (name, role, body, output_model) |
|---|---|---|
Replace (default) | Overlay’s map replaces the base’s map wholesale | Overlay’s value replaces when present |
Merge | Top-level keys union (child-wins on collision, whole entry — no recursion) | Overlay’s value replaces when present |
Replace is the pre-0.3 behavior, kept as the default — no existing call site changes.
Merge is the new strategy. Use it when you want a child prompt that inherits the base’s
declared variables and adds its own — without hand-spreading the base’s variables into every overlay.
In TypeScript and Python the strategy is derive’s options argument ({ strategy } / strategy=);
in Rust it rides in DeriveOptions on derive_with:
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Derive guide — `MergeStrategy::Merge` via `derive_with`: union the base's declared//! variables with the overlay's, so a child prompt inherits `company` + `max_words` and adds//! its own `tone` without hand-spreading the base's variables. The base is untouched.//! Standalone — `cargo run --example guides_derive_merge`.
use prompting_press::{DeriveOptions, MergeStrategy, 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 base = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// Merge unions the map-typed fields (variables/variants/metadata) at their top-level // keys — child-wins on collision. The base's `company` + `max_words` survive; the // overlay only declares what it adds. `derive_with` takes the strategy in DeriveOptions. let child = base.derive_with( PromptOverlay { body: Some( "You are a {{ tone }} assistant for {{ company }}. \ Keep replies under {{ max_words }} words." .to_string(), ), variables: Some(serde_json::from_value(json!({ "tone": { "type": "string", "trusted": true } }))?), ..Default::default() }, DeriveOptions { strategy: MergeStrategy::Merge, }, )?;
// child inherited the base's two variables and gained its own — three in total. assert!(child.variables().contains_key("company")); assert!(child.variables().contains_key("max_words")); assert!(child.variables().contains_key("tone")); assert_eq!(child.variables().len(), 3); // base is untouched: no `tone` leaked back onto it. assert!(!base.variables().contains_key("tone")); Ok(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Derive guide — MergeStrategy.MERGE: union the base's declared variables with theoverlay's, so a child prompt inherits ``company`` + ``max_words`` and adds its own ``tone``without hand-spreading the base's variables. The base is untouched.
Standalone — the docs page displays this file verbatim; run it directly to check."""
from pathlib import Path
from prompting_press import MergeStrategy, 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: base = Prompt.from_yaml((_HERE / "assistant.yaml").read_text())
# MERGE unions the map-typed fields (variables/variants/metadata) at their top-level # keys — child-wins on collision. The base's `company` + `max_words` survive; the # overlay only needs to declare what it adds. child = base.derive( { "body": "You are a {{ tone }} assistant for {{ company }}. " "Keep replies under {{ max_words }} words.", "variables": {"tone": {"type": "string", "trusted": True}}, }, strategy=MergeStrategy.MERGE, )
# child inherited the base's two variables and gained its own — three in total. assert set(child.variables) == {"company", "max_words", "tone"} # base is untouched: no `tone` leaked back onto it. assert "tone" not in base.variables
if __name__ == "__main__": main()// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * Derive guide — MergeStrategy.Merge: union the base's declared variables with the overlay's, * so a child prompt inherits `company` + `max_words` and adds its own `tone` without * hand-spreading the base's variables. The base 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 { MergeStrategy, 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("Merge unions the base's variables into the child, leaving the base untouched", () => { const base = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
// Merge unions the map-typed fields (variables/variants/metadata) at their top-level // keys — child-wins on collision. The base's `company` + `max_words` survive; the // overlay only needs to declare what it adds. `strategy` rides in the options object. const child = base.derive( { body: "You are a {{ tone }} assistant for {{ company }}. Keep replies under {{ max_words }} words.", variables: { tone: { type: "string", trusted: true } }, }, { strategy: MergeStrategy.Merge }, );
// child inherited the base's two variables and gained its own — three in total. assert.deepEqual( new Set(Object.keys(child.variables ?? {})), new Set(["company", "max_words", "tone"]), ); // base is untouched: no `tone` leaked back onto it. assert.ok(!("tone" in (base.variables ?? {})));});Soundness boundary
Section titled “Soundness boundary”The agreement check is name-only (referenced variables ⊆ declared variables). This means:
- A
Mergethat effectively removes a variable a base body or variant still references fails construction (agreement check catches it). - A
Mergethat replaces a variable’s declaration (e.g. changestypeortrusted) is accepted — that is the validator’s responsibility, not the kernel’s.
deep and none are excluded
Section titled “deep and none are excluded”Only Replace and Merge are supported. There is no recursive / deep merge and no no-op mode.
These axes are reserved for a future consumer that earns them; they are not anticipated.
metadata (including guard keys) under Merge
Section titled “metadata (including guard keys) under Merge”Metadata keys union at top-level under Merge. If the overlay supplies a guard key, the overlay’s
whole guard entry replaces the base’s (child-wins-whole-entry). The library does not interpret
metadata contents — the union is opaque.
variants is replaced wholesale under Replace — read, then spread (or use Merge)
Section titled “variants is replaced wholesale under Replace — read, then spread (or use Merge)”Under the default Replace strategy, an overlay’s variants map replaces the entire existing
map. When the prompt already has variants and the intent is to add one while keeping the rest,
either:
- Use
MergeStrategy.Merge/MergeStrategy::Merge— the union is automatic, and - Read the current map with
.variants()and spread it into the overlay — the manual pre-0.3 idiom, still valid underReplace.
.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:
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""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()// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * 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);});// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""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.
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * 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, options?) — overlay is a partial PromptDefinition object; options is
{ validators?, strategy? }. 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):
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""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()// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * 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:
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""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()// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * 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, { validators: newSchema })
(TypeScript) to override. Coverage is re-checked against the merged variable set — not the base’s.
Rendering a named variant
Section titled “Rendering a named variant”After adding a variant, select it by name at render time:
// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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(())}# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""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()// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/** * 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.5.0