lenzcom's picture
Upload folder using huggingface_hub
e706de2 verified

Prompts: Template-Driven LLM Inputs

Part 2: Composition - Lesson 1

Stop hardcoding prompts. Start composing them.

Overview

You've been writing prompts like this:

const prompt = `You are a helpful assistant. The user asked: ${userInput}`;
const response = await llm.invoke(prompt);

This works, but it's fragile. What if you need:

  • Different system messages for different use cases?
  • To inject multiple variables?
  • To reuse prompt patterns across your app?
  • To validate inputs before sending to the LLM?
  • To compose prompts from smaller pieces?

PromptTemplates solve all of these problems.

Why This Matters

The Problem: Prompt Chaos

Without templates, your code becomes a mess:

// Scattered throughout your codebase:
const prompt1 = `Translate to ${lang}: ${text}`;
const prompt2 = "Translate to " + language + ": " + input;
const prompt3 = `Translate to ${target_language}: ${user_text}`;

// Same logic, different implementations everywhere!

Problems:

  • No consistency in prompt format
  • Hard to test prompts in isolation
  • Can't reuse prompt patterns
  • Difficult to track what prompts are being used
  • No validation of variables

The Solution: PromptTemplate

const translatePrompt = new PromptTemplate({
    template: "Translate to {language}: {text}",
    inputVariables: ["language", "text"]
});

const prompt = await translatePrompt.format({
    language: "Spanish",
    text: "Hello, world!"
});
// "Translate to Spanish: Hello, world!"

Benefits:

  • βœ… Reusable prompt patterns
  • βœ… Variable validation
  • βœ… Testable in isolation
  • βœ… Composable with other Runnables
  • βœ… Type-safe variable injection

Learning Objectives

By the end of this lesson, you will:

  • βœ… Build a PromptTemplate class that replaces variables
  • βœ… Create ChatPromptTemplate for structured messages
  • βœ… Implement Few-Shot prompts for examples
  • βœ… Build Pipeline prompts for composition
  • βœ… Use prompts as Runnables in chains
  • βœ… Understand LangChain's prompting patterns

Core Concepts

What is a PromptTemplate?

A PromptTemplate is a reusable prompt pattern with placeholders for variables.

Structure:

Template String: "Translate to {language}: {text}"
                           ↓
Variables Injected: { language: "Spanish", text: "Hello" }
                           ↓
Output: "Translate to Spanish: Hello"

The Prompt Hierarchy

BasePromptTemplate (abstract)
    β”œβ”€β”€ PromptTemplate (string templates)
    β”œβ”€β”€ ChatPromptTemplate (message templates)
    β”œβ”€β”€ FewShotPromptTemplate (examples + template)
    β”œβ”€β”€ PipelinePromptTemplate (compose templates)
    └── SystemMessagePromptTemplate (system message helper)

Each type serves a specific use case.

Key Operations

  1. Format: Replace variables with values
  2. Validate: Check required variables are provided
  3. Compose: Combine templates together
  4. Invoke: Use as a Runnable (returns formatted prompt)

Implementation Guide

Step 1: Base Prompt Template

Location: src/prompts/base-prompt-template.js

This is the abstract base class all prompts inherit from.

What it does:

  • Defines the interface for all prompt templates
  • Extends Runnable (so prompts work in chains)
  • Provides validation logic

Key methods:

  • format(values) - Replace variables and return string/messages
  • formatPromptValue(values) - Return as PromptValue (for messages)
  • _validateInput(values) - Check all required variables present

Implementation:

import { Runnable } from '../core/runnable.js';

/**
 * Base class for all prompt templates
 */
export class BasePromptTemplate extends Runnable {
    constructor(options = {}) {
        super();
        this.inputVariables = options.inputVariables || [];
        this.partialVariables = options.partialVariables || {};
    }

    /**
     * Format the prompt with given values
     * @abstract
     */
    async format(values) {
        throw new Error('Subclasses must implement format()');
    }

    /**
     * Runnable interface: invoke returns formatted prompt
     */
    async _call(input, config) {
        return await this.format(input);
    }

    /**
     * Validate that all required variables are provided
     */
    _validateInput(values) {
        const provided = { ...this.partialVariables, ...values };
        const missing = this.inputVariables.filter(
            key => !(key in provided)
        );

        if (missing.length > 0) {
            throw new Error(
                `Missing required input variables: ${missing.join(', ')}`
            );
        }
    }

    /**
     * Merge partial variables with provided values
     */
    _mergePartialAndUserVariables(values) {
        return { ...this.partialVariables, ...values };
    }
}

Key insights:

  • Extends Runnable so prompts can be chained
  • _call invokes format - this makes prompts work in pipelines
  • Validation ensures all variables are provided
  • Partial variables are pre-filled defaults

Step 2: PromptTemplate

Location: src/prompts/prompt-template.js

The most common prompt template - replaces {variable} placeholders.

What it does:

  • Takes a template string with {placeholders}
  • Replaces placeholders with actual values
  • Validates all variables are provided

Template syntax:

"Hello {name}, you are {age} years old."

Implementation:

import { BasePromptTemplate } from './base-prompt-template.js';

/**
 * Simple string template with {variable} placeholders
 * 
 * Example:
 *   const prompt = new PromptTemplate({
 *       template: "Translate to {language}: {text}",
 *       inputVariables: ["language", "text"]
 *   });
 *   
 *   await prompt.format({ language: "Spanish", text: "Hello" });
 *   // "Translate to Spanish: Hello"
 */
export class PromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.template = options.template;

        // Auto-detect input variables if not provided
        if (!options.inputVariables) {
            this.inputVariables = this._extractInputVariables(this.template);
        }
    }

    /**
     * Format the template with provided values
     */
    async format(values) {
        this._validateInput(values);
        const allValues = this._mergePartialAndUserVariables(values);

        let result = this.template;
        for (const [key, value] of Object.entries(allValues)) {
            const regex = new RegExp(`\\{${key}\\}`, 'g');
            result = result.replace(regex, String(value));
        }

        return result;
    }

    /**
     * Extract variable names from template string
     * Finds all {variable} patterns
     */
    _extractInputVariables(template) {
        const matches = template.match(/\{(\w+)\}/g) || [];
        return matches.map(match => match.slice(1, -1));
    }

    /**
     * Static helper to create from template string
     */
    static fromTemplate(template, options = {}) {
        return new PromptTemplate({
            template,
            ...options
        });
    }
}

Usage example:

const prompt = new PromptTemplate({
    template: "Translate to {language}: {text}",
    inputVariables: ["language", "text"]
});

const formatted = await prompt.format({
    language: "Spanish",
    text: "Hello, world!"
});
// "Translate to Spanish: Hello, world!"

Step 3: ChatPromptTemplate

Location: src/prompts/chat-prompt-template.js

For structured conversations with system/user/assistant messages.

What it does:

  • Creates arrays of Message objects
  • Supports system, human, and AI message templates
  • Returns properly formatted conversation structure

Message template syntax:

[
    ["system", "You are a {role}"],
    ["human", "My question: {question}"],
    ["ai", "Let me help with {topic}"]
]

Implementation:

import { BasePromptTemplate } from './base-prompt-template.js';
import { SystemMessage, HumanMessage, AIMessage } from '../core/message.js';
import { PromptTemplate } from './prompt-template.js';

/**
 * Template for chat-based conversations
 * Returns an array of Message objects
 * 
 * Example:
 *   const prompt = ChatPromptTemplate.fromMessages([
 *       ["system", "You are a {role}"],
 *       ["human", "{input}"]
 *   ]);
 *   
 *   const messages = await prompt.format({
 *       role: "translator",
 *       input: "Hello"
 *   });
 *   // [SystemMessage(...), HumanMessage(...)]
 */
export class ChatPromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.promptMessages = options.promptMessages || [];
    }

    /**
     * Format into array of Message objects
     */
    async format(values) {
        this._validateInput(values);
        const allValues = this._mergePartialAndUserVariables(values);

        const messages = [];
        for (const [role, template] of this.promptMessages) {
            const content = await this._formatMessageTemplate(template, allValues);
            messages.push(this._createMessage(role, content));
        }

        return messages;
    }

    /**
     * Format a single message template
     */
    async _formatMessageTemplate(template, values) {
        if (typeof template === 'string') {
            const promptTemplate = new PromptTemplate({ template });
            return await promptTemplate.format(values);
        }
        return template;
    }

    /**
     * Create appropriate Message object for role
     */
    _createMessage(role, content) {
        switch (role.toLowerCase()) {
            case 'system':
                return new SystemMessage(content);
            case 'human':
            case 'user':
                return new HumanMessage(content);
            case 'ai':
            case 'assistant':
                return new AIMessage(content);
            default:
                throw new Error(`Unknown message role: ${role}`);
        }
    }

    /**
     * Static helper to create from message list
     */
    static fromMessages(messages, options = {}) {
        const promptMessages = messages.map(msg => {
            if (Array.isArray(msg)) {
                return msg; // [role, template]
            }
            throw new Error('Each message must be [role, template] array');
        });

        // Extract all input variables from all templates
        const inputVariables = new Set();
        for (const [, template] of promptMessages) {
            if (typeof template === 'string') {
                const matches = template.match(/\{(\w+)\}/g) || [];
                matches.forEach(m => inputVariables.add(m.slice(1, -1)));
            }
        }

        return new ChatPromptTemplate({
            promptMessages,
            inputVariables: Array.from(inputVariables),
            ...options
        });
    }
}

Usage example:

const chatPrompt = ChatPromptTemplate.fromMessages([
    ["system", "You are a {role} assistant"],
    ["human", "My question: {question}"]
]);

const messages = await chatPrompt.format({
    role: "helpful",
    question: "What's the weather?"
});

// Returns:
// [
//   SystemMessage("You are a helpful assistant"),
//   HumanMessage("My question: What's the weather?")
// ]

Step 4: Few-Shot Prompt Template

Location: src/prompts/few-shot-prompt.js

For including examples in your prompts (few-shot learning).

What it does:

  • Takes a list of examples
  • Formats each example with a template
  • Combines examples with the main prompt

Structure:

Examples:
  Input: 2+2  Output: 4
  Input: 3+5  Output: 8

Prompt: Input: {input}  Output:

Implementation:

import { BasePromptTemplate } from './base-prompt-template.js';
import { PromptTemplate } from './prompt-template.js';

/**
 * Prompt template that includes examples (few-shot learning)
 * 
 * Example:
 *   const fewShot = new FewShotPromptTemplate({
 *       examples: [
 *           { input: "2+2", output: "4" },
 *           { input: "3+5", output: "8" }
 *       ],
 *       examplePrompt: new PromptTemplate({
 *           template: "Input: {input}\nOutput: {output}",
 *           inputVariables: ["input", "output"]
 *       }),
 *       prefix: "Solve these math problems:",
 *       suffix: "Input: {input}\nOutput:",
 *       inputVariables: ["input"]
 *   });
 */
export class FewShotPromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.examples = options.examples || [];
        this.examplePrompt = options.examplePrompt;
        this.prefix = options.prefix || '';
        this.suffix = options.suffix || '';
        this.exampleSeparator = options.exampleSeparator || '\n\n';
    }

    /**
     * Format the few-shot prompt
     */
    async format(values) {
        this._validateInput(values);
        const allValues = this._mergePartialAndUserVariables(values);

        const parts = [];

        // Add prefix
        if (this.prefix) {
            const prefixTemplate = new PromptTemplate({ template: this.prefix });
            parts.push(await prefixTemplate.format(allValues));
        }

        // Add formatted examples
        if (this.examples.length > 0) {
            const exampleStrings = await Promise.all(
                this.examples.map(ex => this.examplePrompt.format(ex))
            );
            parts.push(exampleStrings.join(this.exampleSeparator));
        }

        // Add suffix
        if (this.suffix) {
            const suffixTemplate = new PromptTemplate({ template: this.suffix });
            parts.push(await suffixTemplate.format(allValues));
        }

        return parts.join('\n\n');
    }
}

Usage example:

const fewShotPrompt = new FewShotPromptTemplate({
    examples: [
        { input: "happy", output: "sad" },
        { input: "tall", output: "short" }
    ],
    examplePrompt: new PromptTemplate({
        template: "Input: {input}\nOutput: {output}",
        inputVariables: ["input", "output"]
    }),
    prefix: "Give the antonym of each word:",
    suffix: "Input: {word}\nOutput:",
    inputVariables: ["word"]
});

const prompt = await fewShotPrompt.format({ word: "hot" });

// Output:
// Give the antonym of each word:
//
// Input: happy
// Output: sad
//
// Input: tall
// Output: short
//
// Input: hot
// Output:

Step 5: Pipeline Prompt Template

Location: src/prompts/pipeline-prompt.js

For composing multiple prompts together.

What it does:

  • Combines multiple prompt templates
  • Pipes output of one template to input of another
  • Enables modular prompt construction

Implementation:

import { BasePromptTemplate } from './base-prompt-template.js';

/**
 * Compose multiple prompts into a pipeline
 * 
 * Example:
 *   const pipeline = new PipelinePromptTemplate({
 *       finalPrompt: mainPrompt,
 *       pipelinePrompts: [
 *           { name: "context", prompt: contextPrompt },
 *           { name: "instructions", prompt: instructionPrompt }
 *       ]
 *   });
 */
export class PipelinePromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.finalPrompt = options.finalPrompt;
        this.pipelinePrompts = options.pipelinePrompts || [];

        // Collect all input variables
        this.inputVariables = this._collectInputVariables();
    }

    /**
     * Format by running pipeline prompts first, then final prompt
     */
    async format(values) {
        this._validateInput(values);
        const allValues = this._mergePartialAndUserVariables(values);

        // Format each pipeline prompt and collect results
        const pipelineResults = {};
        for (const { name, prompt } of this.pipelinePrompts) {
            pipelineResults[name] = await prompt.format(allValues);
        }

        // Merge with original values and format final prompt
        const finalValues = { ...allValues, ...pipelineResults };
        return await this.finalPrompt.format(finalValues);
    }

    /**
     * Collect input variables from all prompts
     */
    _collectInputVariables() {
        const vars = new Set(this.finalPrompt.inputVariables);
        
        for (const { prompt } of this.pipelinePrompts) {
            prompt.inputVariables.forEach(v => vars.add(v));
        }

        // Remove pipeline output names (they're generated)
        this.pipelinePrompts.forEach(({ name }) => vars.delete(name));

        return Array.from(vars);
    }
}

Usage example:

const contextPrompt = new PromptTemplate({
    template: "Context: {topic} is important because {reason}",
    inputVariables: ["topic", "reason"]
});

const mainPrompt = new PromptTemplate({
    template: "{context}\n\nQuestion: {question}",
    inputVariables: ["context", "question"]
});

const pipeline = new PipelinePromptTemplate({
    finalPrompt: mainPrompt,
    pipelinePrompts: [
        { name: "context", prompt: contextPrompt }
    ]
});

const result = await pipeline.format({
    topic: "AI",
    reason: "it transforms industries",
    question: "What are the risks?"
});

// Output:
// Context: AI is important because it transforms industries
//
// Question: What are the risks?

Step 6: System Message Prompt Template

Location: src/prompts/system-message-prompt.js

A specialized template for creating system messages with consistent formatting.

What it does:

  • Creates SystemMessage objects with variable substitution
  • Simplifies creating reusable system prompts
  • Provides a cleaner API than ChatPromptTemplate for system-only messages

Why this matters: System messages set the behavior and context for LLMs. Having a dedicated template makes it easier to:

  • Manage different system prompts for different use cases
  • A/B test system message variations
  • Inject context dynamically (user preferences, current date, etc.)
  • Maintain consistent system message formatting

Implementation:

import { BasePromptTemplate } from './base-prompt-template.js';
import { SystemMessage } from '../core/message.js';
import { PromptTemplate } from './prompt-template.js';

/**
 * Template specifically for system messages
 * Returns a single SystemMessage object
 * 
 * Example:
 *   const systemPrompt = new SystemMessagePromptTemplate({
 *       template: "You are a {role} assistant specialized in {domain}. {instructions}",
 *       inputVariables: ["role", "domain", "instructions"]
 *   });
 *   
 *   const message = await systemPrompt.format({
 *       role: "helpful",
 *       domain: "cooking",
 *       instructions: "Always provide recipe alternatives."
 *   });
 *   // SystemMessage("You are a helpful assistant specialized in cooking. Always provide recipe alternatives.")
 */
export class SystemMessagePromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.prompt = options.prompt || new PromptTemplate({
            template: options.template,
            inputVariables: options.inputVariables,
            partialVariables: options.partialVariables
        });
        
        // Inherit input variables from inner prompt
        if (!options.inputVariables) {
            this.inputVariables = this.prompt.inputVariables;
        }
    }

    /**
     * Format into a SystemMessage object
     */
    async format(values) {
        this._validateInput(values);
        const allValues = this._mergePartialAndUserVariables(values);
        
        const content = await this.prompt.format(allValues);
        return new SystemMessage(content);
    }

    /**
     * Static helper to create from template string
     */
    static fromTemplate(template, options = {}) {
        return new SystemMessagePromptTemplate({
            template,
            ...options
        });
    }

    /**
     * Create with partial variables pre-filled
     * Useful for setting default context that can be overridden
     */
    static fromTemplateWithPartials(template, partialVariables = {}, options = {}) {
        const promptTemplate = new PromptTemplate({ template });
        const inputVariables = promptTemplate.inputVariables.filter(
            v => !(v in partialVariables)
        );

        return new SystemMessagePromptTemplate({
            template,
            inputVariables,
            partialVariables,
            ...options
        });
    }
}

Usage Examples:

Example 1: Basic system message

const systemPrompt = SystemMessagePromptTemplate.fromTemplate(
    "You are a {role} assistant. Always be {tone}."
);

const message = await systemPrompt.format({
    role: "helpful",
    tone: "professional"
});
// SystemMessage("You are a helpful assistant. Always be professional.")

Example 2: System message with partial variables (defaults)

const systemPrompt = SystemMessagePromptTemplate.fromTemplateWithPartials(
    "You are a {role} assistant. Today is {date}. User preference: {preference}",
    {
        date: new Date().toLocaleDateString(),  // Pre-filled default
        preference: "concise responses"          // Pre-filled default
    }
);

// Only need to provide 'role' - others use defaults
const message1 = await systemPrompt.format({
    role: "coding"
});
// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: concise responses")

// Can override defaults if needed
const message2 = await systemPrompt.format({
    role: "coding",
    preference: "detailed explanations"
});
// SystemMessage("You are a coding assistant. Today is 11/20/2025. User preference: detailed explanations")

Key Benefits:

  1. Separation of Concerns: System prompts are separate from conversation flow
  2. Reusability: Create once, use in multiple chat templates
  3. A/B Testing: Easily swap system prompts to test effectiveness
  4. Dynamic Context: Inject runtime information (date, user prefs, etc.)
  5. Type Safety: Always returns SystemMessage (not just strings)
  6. Partial Variables: Set defaults that can be overridden

Common Use Cases:

  • Role-based prompting: Different system messages for different assistant personalities
  • Context injection: Add current date, user preferences, session info
  • A/B testing: Test different instruction phrasings
  • Domain switching: Same app, different domains (customer service vs technical support)
  • Compliance: Ensure required disclaimers/policies in every system message

Prompts: Real-World Patterns

Real-World Examples

Example 1: Translation Service

import { PromptTemplate } from './prompts/prompt-template.js';
import { LlamaCppLLM } from './llm/llama-cpp-llm.js';

// Reusable translation prompt
const translationPrompt = new PromptTemplate({
    template: `Translate the following text from {source_lang} to {target_lang}.

Text: {text}

Translation:`,
    inputVariables: ["source_lang", "target_lang", "text"]
});

// Use in your app
const llm = new LlamaCppLLM({ modelPath: './model.gguf' });

// Build a reusable translation chain
const translationChain = translationPrompt.pipe(llm);

// Now you can easily translate
const spanish = await translationChain.invoke({
    source_lang: "English",
    target_lang: "Spanish",
    text: "Hello, how are you?"
});

const french = await translationChain.invoke({
    source_lang: "English",
    target_lang: "French",
    text: "Hello, how are you?"
});

Why this is better:

  • βœ… Prompt pattern defined once, used everywhere
  • βœ… Consistent formatting across translations
  • βœ… Easy to test prompt in isolation
  • βœ… Can swap LLM without changing prompt

Example 2: Customer Support Bot

import { ChatPromptTemplate } from './prompts/chat-prompt-template.js';

const supportPrompt = ChatPromptTemplate.fromMessages([
    ["system", `You are a customer support agent for {company}.

Company details:
- Product: {product}
- Return policy: {return_policy}
- Support hours: {support_hours}

Be helpful, professional, and concise.`],
    ["human", "{customer_message}"]
]);

// Use with partial variables for company info
const myCompanySupportPrompt = supportPrompt.partial({
    company: "TechCorp",
    product: "Cloud Storage",
    return_policy: "30 days",
    support_hours: "9am-5pm EST"
});

// Now only need customer message
const messages = await myCompanySupportPrompt.format({
    customer_message: "How do I cancel my subscription?"
});

const response = await llm.invoke(messages);

Why this is better:

  • βœ… Company info defined once, reused everywhere
  • βœ… Partial variables reduce repetition
  • βœ… Easy to update company policy in one place
  • βœ… Structured conversation format

Example 3: Few-Shot Classification

import { FewShotPromptTemplate } from './prompts/few-shot-prompt.js';
import { PromptTemplate } from './prompts/prompt-template.js';

// Example formatter
const examplePrompt = new PromptTemplate({
    template: "Text: {text}\nSentiment: {sentiment}",
    inputVariables: ["text", "sentiment"]
});

// Few-shot sentiment classifier
const sentimentPrompt = new FewShotPromptTemplate({
    examples: [
        { text: "I love this product!", sentiment: "positive" },
        { text: "This is terrible.", sentiment: "negative" },
        { text: "It's okay, I guess.", sentiment: "neutral" }
    ],
    examplePrompt: examplePrompt,
    prefix: "Classify the sentiment of the following texts:",
    suffix: "Text: {input}\nSentiment:",
    inputVariables: ["input"],
    exampleSeparator: "\n\n"
});

const prompt = await sentimentPrompt.format({
    input: "This exceeded my expectations!"
});

// The LLM now has examples to learn from!
const sentiment = await llm.invoke(prompt);

Why this is better:

  • βœ… Examples teach the LLM the task
  • βœ… More consistent classifications
  • βœ… Easy to add/remove examples
  • βœ… Can dynamically select relevant examples

Example 4: Modular Prompt Composition

import { PipelinePromptTemplate } from './prompts/pipeline-prompt.js';
import { PromptTemplate } from './prompts/prompt-template.js';

// Reusable context builder
const contextPrompt = new PromptTemplate({
    template: `Domain: {domain}
Key concepts: {concepts}
Current date: {date}`,
    inputVariables: ["domain", "concepts", "date"]
});

// Reusable instruction builder
const instructionPrompt = new PromptTemplate({
    template: `Task: {task}
Format: {format}
Constraints: {constraints}`,
    inputVariables: ["task", "format", "constraints"]
});

// Main prompt uses outputs from sub-prompts
const mainPrompt = new PromptTemplate({
    template: `{context}

{instructions}

Input: {input}

Output:`,
    inputVariables: ["context", "instructions", "input"]
});

// Compose them together
const composedPrompt = new PipelinePromptTemplate({
    finalPrompt: mainPrompt,
    pipelinePrompts: [
        { name: "context", prompt: contextPrompt },
        { name: "instructions", prompt: instructionPrompt }
    ]
});

// Use the composed prompt
const result = await composedPrompt.format({
    domain: "Healthcare",
    concepts: "diagnosis, treatment, patient care",
    date: new Date().toISOString().split('T')[0],
    task: "Analyze symptoms",
    format: "JSON with confidence scores",
    constraints: "HIPAA compliant, evidence-based",
    input: "Patient reports fatigue and headaches"
});

Why this is better:

  • βœ… Modular prompt components
  • βœ… Reuse context/instructions across prompts
  • βœ… Easy to update individual sections
  • βœ… Cleaner prompt management

Advanced Patterns

Pattern 1: Conditional Prompts

class ConditionalPromptTemplate extends BasePromptTemplate {
    constructor(options = {}) {
        super(options);
        this.condition = options.condition;
        this.truePrompt = options.truePrompt;
        this.falsePrompt = options.falsePrompt;
    }

    async format(values) {
        const useTrue = this.condition(values);
        const selectedPrompt = useTrue ? this.truePrompt : this.falsePrompt;
        return await selectedPrompt.format(values);
    }
}

// Usage
const prompt = new ConditionalPromptTemplate({
    condition: (values) => values.userType === 'expert',
    truePrompt: new PromptTemplate({
        template: "Technical analysis: {query}"
    }),
    falsePrompt: new PromptTemplate({
        template: "Explain in simple terms: {query}"
    }),
    inputVariables: ["userType", "query"]
});

Pattern 2: Dynamic Examples (Select Best Examples)

class DynamicFewShotPromptTemplate extends FewShotPromptTemplate {
    constructor(options = {}) {
        super(options);
        this.exampleSelector = options.exampleSelector;
        this.maxExamples = options.maxExamples || 3;
    }

    async format(values) {
        // Select most relevant examples dynamically
        const selectedExamples = await this.exampleSelector.select(
            values,
            this.maxExamples
        );

        // Temporarily replace examples
        const originalExamples = this.examples;
        this.examples = selectedExamples;

        const result = await super.format(values);

        // Restore original examples
        this.examples = originalExamples;

        return result;
    }
}

// Example selector based on similarity
class SimilarityExampleSelector {
    constructor(examples) {
        this.examples = examples;
    }

    async select(input, k) {
        // In real implementation, use embeddings for similarity
        // For now, simple keyword matching
        const scores = this.examples.map(ex => ({
            example: ex,
            score: this._similarity(ex.input, input.input)
        }));

        scores.sort((a, b) => b.score - a.score);
        return scores.slice(0, k).map(s => s.example);
    }

    _similarity(a, b) {
        // Simple word overlap
        const wordsA = new Set(a.toLowerCase().split(' '));
        const wordsB = new Set(b.toLowerCase().split(' '));
        const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
        return intersection.size / Math.max(wordsA.size, wordsB.size);
    }
}

Pattern 3: Prompt with Validation

class ValidatedPromptTemplate extends PromptTemplate {
    constructor(options = {}) {
        super(options);
        this.validators = options.validators || {};
    }

    async format(values) {
        // Validate each input
        for (const [key, validator] of Object.entries(this.validators)) {
            if (key in values) {
                const isValid = validator(values[key]);
                if (!isValid) {
                    throw new Error(`Invalid value for ${key}: ${values[key]}`);
                }
            }
        }

        return await super.format(values);
    }
}

// Usage
const emailPrompt = new ValidatedPromptTemplate({
    template: "Send email to {email} about {subject}",
    inputVariables: ["email", "subject"],
    validators: {
        email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
        subject: (value) => value.length > 0 && value.length <= 100
    }
});

// This will throw an error
await emailPrompt.format({
    email: "invalid-email",
    subject: "Hi"
});

Pattern 4: Prompt Chaining

// Chain multiple prompts together
const summaryPrompt = new PromptTemplate({
    template: "Summarize this text: {text}"
});

const bulletPrompt = new PromptTemplate({
    template: "Convert this summary to bullet points:\n{summary}"
});

// Create a chain
const summaryChain = summaryPrompt.pipe(llm);
const bulletChain = bulletPrompt.pipe(llm);

// Use them together
async function summarizeAsBullets(text) {
    const summary = await summaryChain.invoke({ text });
    const bullets = await bulletChain.invoke({ summary });
    return bullets;
}

Common Use Cases

Use Case 1: Multi-Language Support

const prompts = {
    en: new PromptTemplate({
        template: "Answer in English: {query}"
    }),
    es: new PromptTemplate({
        template: "Responde en espaΓ±ol: {query}"
    }),
    fr: new PromptTemplate({
        template: "RΓ©pondre en franΓ§ais: {query}"
    })
};

function getPrompt(language) {
    return prompts[language] || prompts.en;
}

const prompt = getPrompt(userLanguage);
const response = await prompt.pipe(llm).invoke({ query: "Hello" });

Use Case 2: A/B Testing Prompts

const variantA = new PromptTemplate({
    template: "Explain {concept} simply"
});

const variantB = new PromptTemplate({
    template: "Teach me about {concept} like I'm 5 years old"
});

// Randomly select variant
const prompt = Math.random() < 0.5 ? variantA : variantB;

// Track which variant was used
const config = {
    metadata: { variant: prompt === variantA ? 'A' : 'B' }
};

const response = await prompt.pipe(llm).invoke({ concept: "gravity" }, config);

Use Case 3: Prompt Versioning

const prompts = {
    v1: new PromptTemplate({
        template: "Classify: {text}"
    }),
    v2: new PromptTemplate({
        template: "Classify the sentiment of: {text}\nOptions: positive, negative, neutral"
    }),
    v3: ChatPromptTemplate.fromMessages([
        ["system", "You are a sentiment classifier. Only respond with: positive, negative, or neutral"],
        ["human", "{text}"]
    ])
};

// Use specific version
const currentPrompt = prompts.v3;

// Easy to roll back if needed
const response = await currentPrompt.pipe(llm).invoke({ text: "I love this!" });

Best Practices

βœ… DO:

1. Keep prompts in separate files

// prompts/translation.js
export const translationPrompt = new PromptTemplate({
    template: "Translate to {language}: {text}",
    inputVariables: ["language", "text"]
});

// app.js
import { translationPrompt } from './prompts/translation.js';

2. Use partial variables for constants

const prompt = new PromptTemplate({
    template: "Company: {company}\nQuery: {query}",
    partialVariables: {
        company: "MyCompany"
    }
});

3. Test prompts in isolation

const formatted = await prompt.format({ input: "test" });
console.log(formatted);
// Verify output before sending to LLM

4. Version your prompts

// prompts/classifier/v2.js
export const classifierPromptV2 = ...

5. Document your templates

/**
 * Email classification prompt
 * 
 * Variables:
 * - from: Email sender
 * - subject: Email subject
 * - body: Email body
 * 
 * Returns: Category name
 */
export const emailPrompt = ...

❌ DON'T:

1. Hardcode prompts throughout codebase

// Bad
const result = await llm.invoke(`Translate to ${lang}: ${text}`);

2. Skip input validation

// Bad
const prompt = new PromptTemplate({ template: "..." });
await prompt.format({}); // Missing required variables!

3. Make prompts too complex

// Bad - too many nested conditions
const prompt = condition1 ? (condition2 ? prompt1 : prompt2) : ...

4. Forget to handle formatting errors

// Bad
const formatted = await prompt.format(values); // What if this throws?

// Good
try {
    const formatted = await prompt.format(values);
} catch (error) {
    console.error('Prompt formatting failed:', error);
    // Handle gracefully
}

Integration with Framework

Using Prompts in Chains

import { PromptTemplate } from './prompts/prompt-template.js';
import { LlamaCppLLM } from './llm/llama-cpp-llm.js';
import { StringOutputParser } from './output-parsers/string-parser.js';

const prompt = new PromptTemplate({
    template: "Summarize: {text}"
});

const llm = new LlamaCppLLM({ modelPath: './model.gguf' });
const parser = new StringOutputParser();

// Build chain: prompt -> llm -> parser
const chain = prompt.pipe(llm).pipe(parser);

// Use it
const summary = await chain.invoke({ text: "Long article..." });

Using Prompts with Memory

import { ChatPromptTemplate } from './prompts/chat-prompt-template.js';
import { BufferMemory } from './memory/buffer-memory.js';

const memory = new BufferMemory();

const prompt = ChatPromptTemplate.fromMessages([
    ["system", "You are a helpful assistant"],
    ["placeholder", "{chat_history}"],
    ["human", "{input}"]
]);

// Load history from memory
const chatHistory = await memory.loadMemoryVariables();

const messages = await prompt.format({
    chat_history: chatHistory.history,
    input: "What did we discuss?"
});

Testing Prompts

Unit Testing

import { describe, it, expect } from 'your-test-framework';
import { PromptTemplate } from './prompts/prompt-template.js';

describe('PromptTemplate', () => {
    it('should format template with variables', async () => {
        const prompt = new PromptTemplate({
            template: "Hello {name}",
            inputVariables: ["name"]
        });

        const result = await prompt.format({ name: "Alice" });
        expect(result).toBe("Hello Alice");
    });

    it('should throw on missing variables', async () => {
        const prompt = new PromptTemplate({
            template: "Hello {name}",
            inputVariables: ["name"]
        });

        await expect(prompt.format({})).rejects.toThrow();
    });

    it('should handle partial variables', async () => {
        const prompt = new PromptTemplate({
            template: "{greeting} {name}",
            inputVariables: ["greeting", "name"],
            partialVariables: { greeting: "Hello" }
        });

        const result = await prompt.format({ name: "Bob" });
        expect(result).toBe("Hello Bob");
    });
});

Exercises

Practice using prompt templates from simple to complex:

Exercise 17: Using a Basic PromptTemplate

Master variable replacement and template formatting fundamentals.
Starter code: exercises/17-prompt-template.js

Exercise 18: Using the ChatPromptTemplate

Create structured conversations with role-based messages.
Starter code: exercises/18-chat-prompt-template.js

Exercise 19: Using the FewShotPromptTemplate

Implement few-shot learning with examples for better LLM outputs.
Starter code: exercises/19-few-shot-prompt-template.js

Exercise 20: Using the PipelinePromptTemplate

Compose modular prompts by connecting multiple templates.
Starter code: exercises/20-pipeline-prompt-template.js


Summary

You've learned how to build a complete prompting system!

Key Takeaways

  1. PromptTemplate: Basic string templates with variable replacement
  2. ChatPromptTemplate: Structured conversations with role-based messages
  3. FewShotPromptTemplate: Include examples for better LLM performance
  4. PipelinePromptTemplate: Compose prompts modularly
  5. BasePromptTemplate: Foundation that makes prompts Runnables

What You Built

A production-ready prompt system that:

  • βœ… Replaces variables safely
  • βœ… Validates inputs
  • βœ… Composes with other Runnables
  • βœ… Supports chat and completion formats
  • βœ… Enables few-shot learning
  • βœ… Allows prompt composition

Next Steps

Now that you have reusable prompts, you need to parse LLM outputs into structured data.

➑️ Next: Output Parsers

Learn how to extract structured data from LLM responses reliably.


Built with ❀️ for learners who want to understand AI frameworks deeply

← Previous: Context | Tutorial Index | Next: Output Parsers β†’