Advanced Features
Learn about APTL’s advanced capabilities for building sophisticated AI prompt templates.
Template Inheritance
Template inheritance allows you to create reusable base templates that child templates can extend and customize. This is one of APTL’s most powerful features for building modular, maintainable prompt templates.
Here’s a simple example of a base template that defines the structure:
@section identity(overridable=true)
You are an AI assistant.
@end
@section guidelines(overridable=true)
- Be helpful
- Be concise
@end
@section footer
End of prompt
@end
A child template can extend it and customize specific sections:
@extends "base.aptl"
@section identity(override=true)
You are a specialized coding assistant.
@end
Section Merge Strategies
When a child template extends a parent, you can control how sections are combined using different attributes. To completely replace the parent’s section, use override:
@section name(override=true)
This replaces everything from the parent
@end
To add content before the parent’s section, use prepend:
@section name(prepend=true)
This comes first
@end
This produces: Child content + Parent content
To add content after the parent’s section, use append:
@section name(append=true)
This comes last
@end
This produces: Parent content + Child content
Multi-Level Inheritance
APTL supports inheritance chains, allowing you to build complex template hierarchies. Here’s how it works across three levels:
@section base
Base content
@end
@extends "grandparent.aptl"
@section base(append=true)
Parent addition
@end
@extends "parent.aptl"
@section base(append=true)
Child addition
@end
This produces:
Base content
Parent addition
Child addition
Output Formatters
Control how sections are formatted in the output. APTL provides several built-in formatters that automatically handle section formatting, including intelligent heading level management for nested sections.
Plain Formatter (Default)
The plain formatter outputs content without any special formatting:
@section name
Content
@end
Output:
Content
Markdown Formatter
The Markdown formatter automatically adds heading markers based on the section name and intelligently tracks heading levels for nested sections:
@section documentation(format="markdown")
This is the main documentation section.
@section subsection
This subsection gets the appropriate heading level.
@end
@end
Output:
## Documentation
This is the main documentation section.
### Subsection
This subsection gets the appropriate heading level.
The formatter automatically increments heading levels for nested sections, ensuring proper Markdown hierarchy.
JSON Formatter
Format sections as JSON objects:
@section data(format="json")
Content
@end
Output:
{
"data": "Content"
}
Structured Formatter
The structured formatter wraps the top-level section in XML-style tags while using Markdown headings for nested sections. Section names are capitalized in the output:
@section info format="structured"
Main content here.
@section details
Detailed information.
@end
@end
Output:
<info>
Main content here.
## Details
Detailed information.
</info>
Custom Section Titles
Use the title attribute to customize section headings or suppress them entirely:
@section api format="structured" title="API Reference"
Core API documentation.
@end
Output:
<api>
# API Reference
Core API documentation.
</api>
Set title=false to suppress headings without increasing nesting levels:
@section docs format="structured"
Documentation
@section intro title=false
Introduction text without a heading
@section details
Detailed content
@end
@end
@end
Output:
<docs>
Documentation
Introduction text without a heading
## Details
Detailed content
</docs>
Mixed Format Example
You can mix different formatters for different sections. Here’s a powerful example combining structured and markdown formats:
@section outer format="structured"
Outer content
@section "Middle Content Title" format="markdown"
Middle content
@section inner format="structured"
Inner content
@end
@end
@end
Output:
<outer>
Outer content
## Middle Content Title
Middle content
### Inner
Inner content
</outer>
This makes it easy to combine the structure of XML with the readability of Markdown for nested content.
Setting Default Formatter
You can configure a default formatter for your engine:
import { APTLEngine, MarkdownFormatter } from '@finqu/aptl';
const engine = new APTLEngine('gpt-5', {
defaultFormatter: new MarkdownFormatter()
});
Custom Formatters
Create custom output formatters by implementing the OutputFormatter interface:
import { OutputFormatter } from '@finqu/aptl';
class CustomFormatter implements OutputFormatter {
formatSection(name: string, content: string, attributes: Record<string, any>): string {
return `[${name}]\n${content}\n[/${name}]`;
}
formatOutput(sections: Array<{ name: string; content: string }>): string {
return sections.map(s => this.formatSection(s.name, s.content, {})).join('\n\n');
}
}
Template Registry
Manage and organize multiple templates with the Template Registry. This powerful feature allows you to load templates from directories, register them programmatically, and manage template collections efficiently.
Here’s how to set up and use the template registry:
import { APTLEngine, TemplateRegistry } from '@finqu/aptl';
import { LocalFileSystem } from '@finqu/aptl/local-filesystem';
const engine = new APTLEngine('gpt-5');
const fileSystem = new LocalFileSystem('./templates');
const registry = new TemplateRegistry(engine, { fileSystem });
// Load templates from directories
await registry.loadDirectory('base');
await registry.loadDirectory('agents');
// Get a template
const template = registry.get('coding-assistant');
// Render it
const output = await template.render({
agentName: 'CodeBot',
capabilities: ['debugging', 'refactoring']
});
You can also register templates programmatically without loading from files:
registry.register('welcome', `
@section greeting
Welcome, @{user.name}!
@end
`);
To see what templates are available, list them:
const templateNames = registry.list();
console.log('Available templates:', templateNames);
Check if a specific template exists before using it:
if (registry.has('my-template')) {
const template = registry.get('my-template');
// Use template
}
Remove templates when you no longer need them:
registry.unregister('template-name');
Reload templates from tracked directories to pick up changes:
await registry.refresh();
In-Memory File System
For testing or demos, use the in-memory file system instead of loading from disk:
import { ObjectFileSystem } from '@finqu/aptl';
const fileSystem = new ObjectFileSystem({
'template1.aptl': '@section main\nContent\n@end',
'template2.aptl': '@section main\nOther content\n@end'
});
const registry = new TemplateRegistry(engine, fileSystem);
Variable Resolution
Advanced variable resolution features for accessing and validating template data.
The variable resolver is the engine that powers APTL’s data access, supporting complex paths with dot notation, bracket notation, and default values. Here’s how to use it programmatically:
import { VariableResolver } from '@finqu/aptl';
const resolver = new VariableResolver();
const data = {
user: {
profile: {
name: 'Alice',
contacts: ['email@example.com', 'phone']
}
},
items: [
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 }
]
};
// Dot notation
resolver.resolve('user.profile.name', data); // 'Alice'
// Bracket notation
resolver.resolve('items[0].name', data); // 'Item 1'
// Mixed notation
resolver.resolve('user.profile.contacts[0]', data); // 'email@example.com'
// With default values (pipe syntax)
resolver.resolve('user.age|25', data); // 25 (uses default)
resolver.resolve('user.profile.name|"Guest"', data); // 'Alice' (value exists)
resolver.resolve('config.timeout|30', data); // 30 (uses default)
resolver.resolve('settings.debug|false', data); // false (uses default)
Default Value Types
The resolver supports multiple default value types:
- Strings:
@{var|"default"}or@{var|'default'} - Numbers:
@{var|42}or@{var|3.14} - Booleans:
@{var|true}or@{var|false}
Important notes:
- String defaults require quotes:
|"value"not|value - Default is only used when variable is
undefinedornull - Empty strings
""and zero0are valid values and won’t trigger defaults
Extract All Variables
You can extract all variable references from a template string:
const template = `
Hello, @{user.name}!
Your email: @{user.email}
Items: @{items[0].name}
`;
const variables = resolver.extractVariables(template);
// ['user.name', 'user.email', 'items[0].name']
Check if a variable path exists in your data:
const exists = resolver.exists('user.profile.name', data); // true
const missing = resolver.exists('user.invalid.path', data); // false
Validate that all variables in a template exist in your data:
const template = `@{user.name} - @{user.email}`;
const data = { user: { name: 'Alice' } };
const missingVars = resolver.validateTemplate(template, data);
// ['user.email']
Error Handling
APTL provides specific error types for different failure scenarios, making it easier to handle and debug issues.
APTLSyntaxError
This error is thrown when template syntax is invalid:
import { APTLSyntaxError } from '@finqu/aptl';
try {
await engine.render('@if', {});
} catch (err) {
if (err instanceof APTLSyntaxError) {
console.error(`Syntax error at line ${err.line}, column ${err.column}`);
console.error(err.message);
}
}
APTLRuntimeError
This error is thrown during template execution:
import { APTLRuntimeError } from '@finqu/aptl';
try {
await engine.render(template, data);
} catch (err) {
if (err instanceof APTLRuntimeError) {
console.error('Runtime error:', err.message);
console.error('Context:', err.context);
}
}
APTLValidationError
This error is thrown when template validation fails:
import { APTLValidationError } from '@finqu/aptl';
try {
await engine.render(template, data);
} catch (err) {
if (err instanceof APTLValidationError) {
console.error('Validation error:', err.message);
}
}
Error Recovery
Here’s a pattern for comprehensive error handling:
try {
const output = await engine.render(template, data);
return output;
} catch (err) {
if (err instanceof APTLSyntaxError) {
// Handle syntax errors
return 'Template has syntax errors';
} else if (err instanceof APTLRuntimeError) {
// Handle runtime errors
return 'Error executing template';
} else {
// Handle other errors
throw err;
}
}
Custom Directives
Extend APTL with custom directives to add new functionality tailored to your needs. APTL provides three base classes for different directive types, making it easy to create directives that fit seamlessly into the template system.
Directive Base Classes:
InlineDirective- No body, single-line directives (e.g.,@extends,@include,@timestamp)BlockDirective- Has@endterminator for multi-line content (e.g.,@section,@uppercase)ConditionalDirective- Block with branching logic (e.g.,@if,@each,@switch)
Here’s a simple inline directive that outputs the current timestamp:
import { InlineDirective, DirectiveContext } from '@finqu/aptl';
class TimestampDirective extends InlineDirective {
get name(): string {
return 'timestamp';
}
execute(context: DirectiveContext): string {
return new Date().toISOString();
}
}
Usage in a template:
Current time: @timestamp
Block directives can process multi-line content. Here’s a directive that converts its content to uppercase:
import { BlockDirective, DirectiveContext } from '@finqu/aptl';
class UppercaseDirective extends BlockDirective {
get name(): string {
return 'uppercase';
}
execute(context: DirectiveContext): string {
const content = this.renderChildren(context);
return content.toUpperCase();
}
}
Usage in a template:
@uppercase
this will be uppercase
@end
Registering Custom Directives
After creating a custom directive, you need to register it with both the directive registry and the tokenizer:
import { APTLEngine } from '@finqu/aptl';
const engine = new APTLEngine('gpt-5');
const directive = new TimestampDirective();
// Register with both registry and tokenizer
engine.directiveRegistry.register(directive);
engine.tokenizer.registerDirective(directive.name);
Directive with Arguments
For more advanced use cases, you can create directives that accept and parse arguments:
class RepeatDirective extends BlockDirective {
get name(): string {
return 'repeat';
}
parseArguments(argsString: string): any {
const count = parseInt(argsString.trim(), 10);
return { count };
}
execute(context: DirectiveContext): string {
const { count } = this.node.parsedArgs;
const content = this.renderChildren(context);
return Array(count).fill(content).join('\n');
}
}
Usage in a template:
@repeat 3
Repeated line
@end
This produces the output:
Repeated line
Repeated line
Repeated line