LLMPromptTag
A lightweight utility for building clean, composable, and maintainable LLM prompts using tagged template literals.
import { prompt } from 'llm-prompt-tag'; const problems = [ "String concatenation", "Conditional logic mess", "Formatting inconsistencies", "Hard to maintain" ]; const solutions = [ "Tagged templates", "Conditional sections", "Automatic formatting", "Composable structure" ]; const includeDebugInfo = false; const msg = prompt()` You are a helpful AI assistant. It can be really annoying to put all the conditional data in your prompts. ${prompt('Problems with creating prompts')`${problems}`} ${prompt('Solutions from llm-prompt-tag')`${solutions}`} ${prompt('Debug Info', includeDebugInfo)`This section only appears when debugging`} Now I can make complex, conditional, formatted prompts I can still read thanks to llm-prompt-tag! `; console.log(msg);
Output:
You are a helpful AI assistant. It can be really annoying to put all the conditional data in your prompts.
==== Problems with creating prompts ====
String concatenation
Conditional logic mess
Formatting inconsistencies
Hard to maintain
==== End of Problems with creating prompts ====
==== Solutions from llm-prompt-tag ====
Tagged templates
Conditional sections
Automatic formatting
Composable structure
==== End of Solutions from llm-prompt-tag ====
Now I can make complex, conditional, formatted prompts I can still read thanks to llm-prompt-tag!
✨ Features
- ✅ Native JavaScript/TypeScript tagged template syntax
- ✅ Automatic whitespace and formatting cleanup
- ✅ Optional section headers
- ✅ Conditional rendering
- ✅
makePromptgenerator for type-safe extensibility - ✅ Smart array formatting with customizable separators
- ✅ No dependencies
📦 Installation
npm install llm-prompt-tag
🧠 Basic Usage
import { prompt } from 'llm-prompt-tag'; const result = prompt('User Instructions')` Write a short, direct note using the user's own words. `; console.log(result);
Output:
==== User Instructions ====
Write a short, direct note using the user's own words.
==== End of User Instructions ====
🔁 Conditional Sections
const showHelp = false; const result = prompt('Help Section', showHelp)` If you get stuck, refer to the user's original notes. `; console.log(result);
Output:
(empty string - no output because condition is false)
With condition true:
const showHelp = true; const result = prompt('Help Section', showHelp)` If you get stuck, refer to the user's original notes. `; console.log(result);
Output:
==== Help Section ====
If you get stuck, refer to the user's original notes.
==== End of Help Section ====
🏷️ Semantic Naming Patterns
Some people may prefer to rename the function for two distinct purposes: a simpler tag p for the overall template, and a separate one for sections.
This approach can be more readable and semantic:
import { prompt } from 'llm-prompt-tag'; // Use 'section' for structured sections const section = prompt; // Use a plain wrapper for the main content const p = prompt(); const someVariable = "User prefers dark mode"; const comments = "Remember to save work frequently"; const result = p`You are a helpful AI assistant. ${section('User Settings')`${someVariable}`} ${section('Comments')`The user's comments are: ${comments}`} Whatever approach feels cleaner to you!`; console.log(result);
Output:
You are a helpful AI assistant.
==== User Settings ====
User prefers dark mode
==== End of User Settings ====
==== Notes ====
The user's notes are: Remember to save work frequently
==== End of Notes ====
Whatever approach feels cleaner to you!
🧱 Extending with makePrompt
If you have custom objects you're putting into your prompts you can make that easier/cleaner by using the makePrompt generator with type guards and their formatters.
Example:
import { makePrompt } from 'llm-prompt-tag'; type Note = { title: string; content: string; } // Type guard function to check if an object is a Note const isNote = (obj: any): obj is Note => obj && typeof obj.title === 'string' && typeof obj.content === 'string'; const promptWithNotes = makePrompt([ [isNote, (note: Note) => `• ${note.title}\n${note.content}`] ]); const note: Note = { title: "LLM Summary", content: "LLMs are transforming software development." }; const result = promptWithNotes()` Here's a user note: ${note} `; console.log(result);
Output:
Here's a user note:
• LLM Summary
LLMs are transforming software development.
Fallback to toString():
Objects that don't match any registered type guard will automatically use their toString() method, making it easy to mix custom formatted objects with other types:
const customObj = { value: "important data", toString() { return `CustomObject: ${this.value}`; } }; const result = promptWithNotes()` ${note} ${customObj} ${"plain string"} ${42} `;
Output:
• LLM Summary
LLMs are transforming software development.
CustomObject: important data
plain string
42
📝 Smart Array Formatting
To keep templates simpler, arrays of registered types are automatically formatted. By default, items are separated with double newlines for clean readability:
type Note = { title: string; content: string; } const isNote = (obj: any): obj is Note => obj && typeof obj.title === 'string' && typeof obj.content === 'string'; const promptWithNotes = makePrompt([ [isNote, (note: Note) => `• ${note.title}\n ${note.content}`] ]); const notes: Note[] = [ { title: "Meeting", content: "Discuss project timeline" }, { title: "Task", content: "Review pull request" }, { title: "Reminder", content: "Update documentation" } ]; const result = promptWithNotes()`${notes}`; console.log(result);
Output:
• Meeting
Discuss project timeline
• Task
Review pull request
• Reminder
Update documentation
Custom Array Formatting:
You can customize how arrays are formatted (default is two line seperation: '\n\n') by providing an arrayFormatter option:
const commaPrompt = makePrompt( [[isNote, (note: Note) => note.title]], { arrayFormatter: (items: any[], formatter: any) => items.map(formatter).join(', ') } ); const result = commaPrompt()` Meeting topics: ${notes} `; console.log(result);
Output:
Meeting topics: Meeting, Task, Reminder
Mixed-Type Arrays:
Arrays can contain different types, and each item will be formatted according to its type:
type Task = { name: string; completed: boolean; } const isTask = (obj: any): obj is Task => obj && typeof obj.name === 'string' && typeof obj.completed === 'boolean'; const mixedPrompt = makePrompt([ [isNote, (note: Note) => `📝 ${note.title}`], [isTask, (task: Task) => `${task.completed ? '✅' : '⏳'} ${task.name}`] ]); const note: Note = { title: "Meeting", content: "Important discussion" }; const task: Task = { name: "Review code", completed: false }; const customObj = { toString() { return "Custom item"; } }; const mixedArray = [note, task, customObj, "plain string"]; const result = mixedPrompt()`${mixedArray}`; console.log(result);
Output:
📝 Meeting
⏳ Review code
Custom item
plain string
🧪 Multiple formatters example
type Task = { name: string; completed: boolean; } const isTask = (obj: any): obj is Task => obj && typeof obj.name === 'string' && typeof obj.completed === 'boolean'; const customPrompt = makePrompt([ [isNote, (note: Note) => `📝 ${note.title}: ${note.content}`], [isTask, (task: Task) => `${task.completed ? '✅' : '⏳'} ${task.name}`] ]); const note: Note = { title: "Meeting", content: "Discuss project timeline" }; const task: Task = { name: "Review code", completed: false }; const result = customPrompt()` Current items: ${note} ${task} `; console.log(result);
Output:
Current items:
📝 Meeting: Discuss project timeline
⏳ Review code
🔧 Custom Object Formatting
You can customize how unregistered objects are formatted using the objectFormatter option. This is useful for JSON output, custom serialization, or specialized object representations:
JSON Formatting:
const jsonPrompt = makePrompt([ [isNote, (note: Note) => `• ${note.title}`] ], { objectFormatter: (obj) => JSON.stringify(obj, null, 2) }); const unknownObj = { type: "unknown", data: [1, 2, 3] }; const note: Note = { title: "Known object", content: "Has custom formatter" }; const result = jsonPrompt()` ${note} ${unknownObj} `; console.log(result);
Output:
• Known object
{
"type": "unknown",
"data": [
1,
2,
3
]
}
Using toJSON():
const toJSONPrompt = makePrompt([], { objectFormatter: (obj: any) => obj.toJSON ? obj.toJSON() : obj.toString() }); const customObj = { value: "secret", toJSON() { return `JSON: ${this.value}`; } }; const result = toJSONPrompt()`${customObj}`; // Output: "JSON: secret"
🔗 Composable Prompts
Prompts can be nested and composed together:
const systemPrompt = prompt('System')` You are a helpful AI assistant. `; const userContext = prompt('User Context')` User: Alice Task: Help prioritize work `; const fullPrompt = prompt()` ${systemPrompt} ${userContext} Please provide helpful guidance. `; console.log(fullPrompt);
Output:
==== System ====
You are a helpful AI assistant.
==== End of System ====
==== User Context ====
User: Alice
Task: Help prioritize work
==== End of User Context ====
Please provide helpful guidance.
🧪 Example Unit Tests
The library includes comprehensive tests that demonstrate usage patterns:
import { prompt, makePrompt } from 'llm-prompt-tag'; // Basic usage const result = prompt('Intro')` Hello world. `; // Output: "==== Intro ====\nHello world.\n==== End of Intro ====" // With custom formatters using array of tuples type Note = { title: string; content: string; } const isNote = (obj: any): obj is Note => obj && typeof obj.title === 'string' && typeof obj.content === 'string'; const noteFormatter = (note: Note) => `• ${note.title}\n${note.content}`; const customPrompt = makePrompt([ [isNote, noteFormatter] ]); const note: Note = { title: "Meeting", content: "Discuss project timeline" }; const formatted = customPrompt()`${note}`; // Output: "• Meeting\nDiscuss project timeline"
📝 API Reference
prompt(header?, condition?)
Creates a tagged template literal function for building prompts.
Parameters:
header(optional): String to use as section headercondition(optional): Boolean to conditionally render the section (default:true)
Returns: A tagged template function
makePrompt(formatters, options?)
Creates a customizable prompt function with type-specific formatters.
Parameters:
formatters: Array of tuples[typeGuard, formatter]wheretypeGuardis a function that checks types andformatteris a function that formats valuesoptions(optional): Configuration object with optional properties:arrayFormatter: Custom function for formatting arrays (default: join with'\n\n')objectFormatter: Custom function for formatting objects (default: usetoString())
Returns: A prompt function with custom formatting capabilities
Example:
type Note = { title: string; content: string; } const isNote = (obj: any): obj is Note => obj && typeof obj.title === 'string' && typeof obj.content === 'string'; const customPrompt = makePrompt([ [isNote, (note: Note) => `• ${note.title}\n${note.content}`] ], { arrayFormatter: (items, formatter) => items.map(formatter).join(' | '), objectFormatter: (obj) => JSON.stringify(obj, null, 2) });
🏗 Development
# Install dependencies npm install # Build the library npm run build # Run tests npm test # Development mode with watch npm run dev
🧩 Core Implementation
The library consists of two main modules:
prompt.ts
Core tagged template implementation with whitespace cleanup and conditional rendering.
makePrompt.ts
Extension mechanism for registering custom object formatters.
🧾 License
MIT
📢 Author
Dave Fowler
GitHub | X / Twitter | ThingsILearned