amadaius
v1.3.9
Published
Amadaius is a simple and lightweight prompt-templating engine.
Maintainers
Readme
Amadaius
This package is maintained inside the monorepo at
packages/amadaiusand is built with pnpm and Turborepo.
Amadaius is a TypeScript/JavaScript library designed to simplify and streamline the process of creating text-based prompts for AI applications. By separating data validation and transformation and prompt structure, Amadaius ensures your prompts are robust, reusable, and easy to manage.
Amadaius leverages:
- Zod for data validation and transformations.
- Handlebars for templating.
- Optional custom helpers that can be easily plugged in.
Why Use Amadaius?
- Separation of Concerns: Keep your prompt content and structure independent, making it easier to update, localize, and reuse templates.
- Validation and Transformation: Ensure your data is always in the correct format with Zod's powerful schema validation and enrichment features.
- Dynamic Templating: Use Handlebars for conditional logic, loops, and custom helpers to create flexible and adaptable prompts.
- Modular Template Composition: Build complex prompt templates seamlessly from smaller prompt templates.
- Incremental Application: Build complex prompts step-by-step with partial templates, allowing you to fill in data incrementally.
- Async Support: Handle asynchronous data fetching and transformations effortlessly.
TL;DR
Amadaius enables you to create prompts that are validated, enriched, and dynamically generated with minimal effort. It's ideal for building AI applications that require structured and reusable prompts.
Table of Contents
Concepts
Prompt Structure and Content
- Prompt Structure: How the content of the prompt is laid out, defined using a Handlebars template string.
- Prompt Content: The validated and enriched data provided to the template to populate the structure.
Validation and Enrichment with Zod
- Validation: Ensures the data adheres to the expected shape and constraints.
- Enrichment: Transforms or adds data using Zod's
transformmethod.
Handlebars Helpers
Custom functions injected into templates to add dynamic behavior.
Separation of Concerns
Amadaius emphasizes keeping content (data) and structure (template) independent, enabling easier reuse and localization.
Modular Template Composition
Use the asSchema method to compose smaller prompt templates into larger, more complex templates, promoting modularity and reusability.
Features
- Zod Validation: Ensure your template data is correct.
- Handlebars-based Templating: Use Handlebars features like conditionals, loops, helpers, and comments.
- Partial Prompt Templates: Build complex prompts incrementally, adding or overriding data fields at each step.
- Asynchronous Support: Seamlessly handle asynchronous data transformations (
buildAsync(data)). - Modular Composition: Combine smaller prompt templates into larger ones using
asSchema().
Installation
# Using pnpm
pnpm add amadaius
# Using npm
npm install amadaiusBasic Usage
Creating a Prompt Template
- Define a schema describing the data your prompt needs.
- Define a template string (Handlebars syntax) that references properties in the schema.
- Create a
PromptTemplateusingpromptTemplate(schema, templateStr, options).
import { promptTemplate } from "amadaius";
import { z } from "zod";
// Template references `{{topic}}`
const pt = promptTemplate(
z.object({ topic: z.string() }),
"Write a story about {{topic}}!",
);
// Provide data matching the schema
const { prompt, metadata } = pt.build({ topic: "dragons" });
console.log(prompt);
// -> "Write a story about dragons!"
console.log(metadata);
// -> {
// type: "full",
// templateStr: "Write a story about {{topic}}!",
// data: { topic: "dragons" }
// }Validating and Transforming Data
Zod can do more than just type-check. You can refine, transform, and set default values. If data fails validation, an error is thrown.
import { promptTemplate } from "amadaius";
import { z } from "zod";
// Example of refining schema
const pt = promptTemplate(
z
.object({
id: z.string(),
name: z.string(),
})
.refine((data) => data.id.length === 10, {
message: "User ID must be exactly 10 characters long",
}),
"Hello, {{name}}! Your ID is {{id}}.",
);
try {
const { prompt } = pt.build({ id: "0123456789", name: "Alice" });
console.log(prompt);
// -> "Hello, Alice! Your ID is 0123456789."
pt.build({ id: "short", name: "Invalid" }); // This will throw a Zod validation error
} catch (error) {
console.error(error);
// -> ZodError: User ID must be exactly 10 characters long
}You can also transform data using Zod's transform method.
const pt = promptTemplate(
z.string().transform((topic) => ({ topic })), // transforms a string into { topic },
"Write a story about {{topic}}!",
);
// We can pass just a string; the schema transforms it into { topic }
const { prompt } = pt.build("dinosaurs");
console.log(prompt);
// -> "Write a story about dinosaurs!"Composing Prompt Templates
You can convert a PromptTemplate into a Zod schema using asSchema(). This allows you to compose prompt templates together.
import { promptTemplate } from "amadaius";
import { z } from "zod";
// Define smaller prompt templates
const pt1 = promptTemplate(z.object({ name: z.string() }), "Hello, {{name}}!");
const pt2 = promptTemplate(z.object({ question: z.string() }), "{{question}}");
// Compose them into a single prompt
const { prompt } = promptTemplate(
z.object({ greeting: pt1.asSchema(), request: pt2.asSchema() }),
"{{greeting}} {{request}}",
).build({
greeting: { name: "Alice" },
request: { question: "What is your favorite color?" },
});
console.log(prompt);
// -> "Hello, Alice! What is your favorite color?"Partial Templates
Sometimes you need to partially apply data to a template and fill in the rest later. You can convert a PromptTemplate into a PartialPromptTemplate using asPartial() and fill in data incrementally with partial(data).
import { promptTemplate } from "amadaius";
import { z } from "zod";
const pt = promptTemplate(
z.object({
persona: z.string(),
message: z.string(),
}),
"You are {{persona}}. Respond to: {{message}}",
);
// Convert to partial template
const partialPt = pt.asPartial();
// Fill data in multiple steps
partialPt.partial({ persona: "a knowledgeable AI librarian" });
partialPt.partial({
message: "What are the best science fiction books?",
});
// When you're ready, build the final string
const { prompt } = partialPt.build();
console.log(prompt);
// -> "You are a knowledgeable AI librarian. Respond to: What are the best science fiction books?"You can also copy a PartialPromptTemplate to create a new instance with the same data.
const partialPtCopy = partialPt.copy();
const { prompt: copyPrompt } = partialPtCopy.build();
console.log(copyPrompt);
// -> "You are a knowledgeable AI librarian. Respond to: What are the best science fiction books?"
// partialPromptCopy shares the same partial data initially, then you can branch outCustom Handlebars Helpers
You can add custom Handlebars helpers to your templates by passing them in the helpers option.
import { promptTemplate } from "amadaius";
const pt = promptTemplate(
z.object({
persona: z.string(),
user_message: z.string(),
tone: z.enum(["formal", "casual", "enthusiastic"]),
}),
`
You are a helpful AI assistant who always follows the persona and tone specified below.
Persona: {{persona}}
User said: "{{transformTone user_message tone}}"
Please respond to the user's message in a manner consistent with the persona and tone above.
`,
{
helpers: {
transformTone: (
message: string,
tone: "formal" | "casual" | "enthusiastic",
) => {
switch (tone) {
case "formal":
return `Good day. I would like to bring to your attention: ${
message.charAt(0).toUpperCase() + message.slice(1)
}.`;
case "casual":
return `Hey! So basically: ${message}`;
case "enthusiastic":
return `Wow, check this out: ${message}!!!`;
default:
return message;
}
},
},
},
);
const { prompt } = pt.build({
persona: "A knowledgeable librarian",
user_message: "could you help me find a good science fiction book?",
tone: "enthusiastic",
});
console.log(prompt);
// -> `You are a helpful AI assistant who always follows the persona and tone specified below.
// Persona: A knowledgeable librarian
//
// User said: "Wow, check this out: could you help me find a good science fiction book?!!!"
//
// Please respond to the user's message in a manner consistent with the persona and tone above.
// `Amadaius supports asynchronous data transformations using buildAsync(data).
import { promptTemplate } from "amadaius";
const asyncPt = promptTemplate(
z
.object({
productNumber: z.number(),
})
.transform(async ({ productNumber }) => {
await getProductData(productNumber);
return {
productNumber,
productData: JSON.stringify(productData, null, 2),
};
}),
"Act as an expert in creating product descriptions.\n\nProduct {{productNumber}}:\n\n{{productData}}\n\nCreate a product description based on the data provided.",
);
const { prompt } = await asyncPt.buildAsync({ productNumber: 1234 });
console.log(prompt);
// -> "Act as an expert in creating product descriptions.\n\nProduct 1234:\n\n{ ... }\n\nCreate a product description based on the data provided."API Reference
promptTemplate(schema, templateStr, options?)
Creates a new PromptTemplate instance.
Signature
function promptTemplate<TSchema extends ZodType<any, any>>(
schema: TSchema,
templateStr: string,
options?: PromptTemplateOptions,
): PromptTemplate<TSchema>;Parameters
schema: A Zod schema describing the shape of the data needed by your template.templateStr: A Handlebars template string.options?: Optional configuration:helpers?: Record<string, (...args: any) => any>: A key-value map of custom Handlebars helpers.
Returns
- A new
PromptTemplateinstance.
Class: PromptTemplate<TSchema>
A fully specified prompt template. You create an instance of this class using promptTemplate.
build(data)
Builds the prompt string using the provided data.
Signature
build(data: z.input<TSchema>): PromptTemplateBuildResult<TSchema>;Parameters
data: Data matching the schema.
Returns
An object containing:
prompt: The rendered template stringmetadata: Object containing:type: "full" | "partial"templateStr: Original template stringdata: The validated/transformed data- Optional fields:
templateId,experimentId,version,description,custom
Throws
- Zod validation errors if
datadoesn't match the schema.
buildAsync(data)
Builds the prompt string asynchronously using the provided data. This enables asynchronous data transformations (e.g., when using z.transform(async ...) in Zod).
Signature
async buildAsync(data: z.input<TSchema>): Promise<PromptTemplateBuildResult<TSchema>>;Parameters
data: Data matching the schema.
Returns
A promise that resolves to an object containing:
prompt: The rendered template stringmetadata: Object containing:type: "full" | "partial"templateStr: Original template stringdata: The validated/transformed data- Optional fields:
templateId,experimentId,version,description,custom
Throws
- Zod validation errors if
datadoesn't match the schema.
asSchema()
Enables prompt template composition by converting the PromptTemplate into a zod schema with a built-in transform.
Signature
asSchema(): ZodType<z.input<TSchema>, string, unknown>;Returns
- A new Zod schema with a built-in
transformmethod that converts the data into a built prompt.
asPartial()
Returns a PartialPromptTemplate based on a PromptTemplate, allowing you to partially apply data over multiple steps.
Signature
asPartial(): PartialPromptTemplate<TSchema>;Returns
- A new
PartialPromptTemplateinstance.
Class: PartialPromptTemplate<TSchema>
A template that can be progressively filled with data before finalising. It has the same underlying schema as the original PromptTemplate.
partial(data)
Partially applies data to the template.
Signature
partial(data: DeepPartial<z.input<TSchema>>): PartialPromptTemplate<TSchema>;Parameters
data: A partial version of whatTSchemaexpects. Each call merges with existing partial data.
Returns
- The
PartialPromptTemplateinstance.
build()
Finalises the partial data, validates it with the original schema, and compiles the template into a string. Throws Zod validation error if the partial data is not sufficient or invalid.
Signature
build(): PromptTemplateBuildResult<TSchema>;Returns
An object containing:
prompt: The rendered template stringmetadata: Object containing:type: "partial"templateStr: Original template stringdata: The validated/transformed data- Optional fields:
templateId,experimentId,version,description,custom
Throws
- Zod validation errors if the partial data doesn't match the schema.
buildAsync()
Finalises the partial data, validates it with the original schema, and compiles the template into a string asynchronously.
Signature
async buildAsync(): Promise<PromptTemplateBuildResult<TSchema>>;Returns
A promise that resolves to an object containing:
prompt: The rendered template stringmetadata: Object containing:type: "partial"templateStr: Original template stringdata: The validated/transformed data- Optional fields:
templateId,experimentId,version,description,custom
Throws
- Zod validation errors if the partial data doesn't match the schema.
copy()
Creates a new PartialPromptTemplate instance with the same partial data. Useful for creating branches from a partially-applied template without interfering with each other's data.
Release Process
Releases are managed through two separate GitHub Actions workflows:
1. Release Workflow
The Release workflow handles versioning and creates the GitHub release. To create a new release:
- Go to the Actions tab in the GitHub repository
- Select the "Release" workflow from the left sidebar
- Click "Run workflow" and select the branch (usually
main) - Enter the version number in semver format (e.g.,
1.2.3,2.0.0,1.2.3-beta.1) - Click "Run workflow" to start the release process
The Release workflow will:
- ✅ Build and verify the project
- ✅ Check if the version already exists (prevents duplicate releases)
- ✅ Bump the version in
package.jsonandpnpm-lock.yaml - ✅ Create a git tag with the version
- ✅ Push the changes and tag to the repository
- ✅ Create a GitHub release
- ✅ Automatically trigger the Publish workflow
2. Publish Workflow
The Publish workflow handles publishing to npm. It runs automatically after a successful Release workflow, but can also be manually triggered if needed (e.g., if the initial publish failed due to authentication issues). The workflow uses pnpm to install dependencies, build, test, and publish the package.
The Publish workflow will:
- ✅ Build and verify the package
- ✅ Verify npm authentication
- ✅ Publish the package to npm
To manually trigger the Publish workflow:
- Go to the Actions tab
- Select the "Publish" workflow
- Click "Run workflow"
- Optionally specify a version (defaults to the version in
package.json)
Note: Both workflows use npm's trusted publishing (OIDC) for secure authentication. To set it up:
- Go to https://www.npmjs.com/settings/YOUR_USERNAME/automation
- Click "Add GitHub Actions" or "Add Trusted Publisher"
- Enter the following details:
- Organization/User:
samueljacobs98 - Repository:
samueljacobs98/amadaius - Workflow filename:
publish.yml(must match exactly, including case!) - Environment name: (leave blank)
- Organization/User:
- Grant publish permissions
⚠️ Important: The workflow filename must be exactly publish.yml for trusted publishing to work.
No NPM_TOKEN secret is needed! Trusted publishing is more secure and future-proof than classic tokens.
Contributing
Contributions are welcome! Please read the contribution guidelines first.
License
This project is licensed under the MIT License. See the LICENSE file for details.
