| name | empathic-templates |
| description | Smart templates that understand semantic intent, not just string substitution |
| license | MIT |
| tier | 1 |
| allowed-tools | ["read_file","write_file"] |
| related | ["moollm","empathic-expressions","postel","yaml-jazz","skill","incarnation","character","coherence-engine"] |
| tags | ["moollm","generation","semantic","substitution","understanding"] |
Empathic Templates
"Templates that understand what you mean, not just what you wrote."
What Is It?
Empathic Templates are MOOLLM's approach to template instantiation: templates that leverage LLM comprehension to understand semantic intent, not just perform mechanical string substitution.
Traditional templates: {{name}} → replace with literal value
Empathic templates: {{name}} → understand what name means in context, generate appropriate content
The Difference
Traditional Templates (Handlebars, Jinja, etc.)
Hello {{name}}!
Your order {{order_id}} has shipped.
Items: {{items}}
Total: ${{total}}
Problem: If items is a list, you need explicit loop syntax. If total should be formatted, you need filters. The template is dumb.
Empathic Templates
greeting: "{{appropriate_greeting}}"
order_summary: "{{summarize_order_naturally}}"
items: "{{list_items_with_quantities}}"
total: "{{format_currency(total)}}"
The LLM doesn't just substitute — it interprets:
{{appropriate_greeting}} becomes "Good morning, Sarah!" or "Hey there!" based on context
{{summarize_order_naturally}} becomes prose, not data dump
{{list_items_with_quantities}} formats intelligently
How It Works
1. Parse Template
template: |
# {{character_name}}
{{describe_appearance}}
{{personality_as_prose}}
> "{{generate_catchphrase}}"
2. Understand Context
context:
character_type: "grumpy wizard"
setting: "medieval fantasy"
tone: "humorous"
age: "ancient"
3. Semantic Generation
The LLM doesn't just fill slots — it creates coherent content:
# Grumbald the Insufferable
A stooped figure wrapped in robes that were probably purple
once, before centuries of potion stains and cat hair took
their toll. His beard reaches his knees and contains at
least three quills, a small bird's nest, and what might be
a sandwich from the Third Age.
## Personality
Grumbald has perfected the art of making everyone feel
slightly stupid. He answers questions with questions,
sighs dramatically at simple requests, and has been
known to turn people into newts for using incorrect
grammar. Deep down, he cares — but he'd rather be
turned into a newt himself than admit it.
## Catchphrase
> "I've forgotten more magic than you'll ever know.
> Literally. Where did I put my staff?"
Template Syntax
Basic Slots
{{variable}}
{{expression}}
{{
{{
{{> partial}}
Empathic Slots (The Magic)
{{describe_X}}
{{summarize_Y}}
{{generate_Z}}
{{appropriate_W}}
{{format_V}}
Expressions with Empathic Interpretation
{{
{{describe_hunger_behavior}}
{{/if}}
{{
{{guard_friendly_greeting}}
{{else}}
{{guard_suspicious_challenge}}
{{/if}}
The conditions use Empathic Expressions for flexible interpretation.
Used For
Character Creation
id: {{generate_unique_id}}
name: "{{character_name}}"
species: "{{species}}"
description: |
{{describe_appearance_based_on_species_and_personality}}
personality:
{{infer_sims_traits_from_description}}
catchphrase: "{{generate_fitting_catchphrase}}"
backstory: |
{{generate_backstory_consistent_with_setting}}
Room Generation
room:
name: "{{room_name}}"
type: {{room_type}}
description: |
{{describe_room_atmospherically}}
{{describe_notable_features}}
{{hint_at_secrets_if_any}}
exits:
{{generate_exits_based_on_maze_topology}}
objects:
{{place_appropriate_objects}}
Session Logs
{{summarize_session_events}}
{{list_memorable_moments}}
{{document_world_changes}}
{{suggest_continuation_hooks}}
The Empathic Expression Connection
Templates use Empathic Expressions for:
Variables
{{user.name}}
{{user.status | capitalize}}
{{player.has_item('key')}}
Conditions
{{
{{
{{
{{
Iterations
{{
- {{this.name}}: {{this.description}}
{{/each}}
Code-Switching IN Templates
query: |
{{empathic_sql: "get all users who ordered in the last month"}}
email: |
Dear {{user.name}},
{{summarize_monthly_orders_naturally}}
Total spent: {{format_currency(monthly_total)}}
Template Discovery Pattern
First 50 lines sniff:
Templates should front-load:
- Template metadata (name, purpose, required context)
- Required variables list
- Optional variables with defaults
id: {{generate_unique_id}}
name: "{{character_name}}"
LLM can sniff first 50 lines to understand what the template needs before reading the full file.
Comment Intelligence
The LLM distinguishes between meta-comments (instructions for generation) and concrete comments (meant for output):
Meta-Comments (Stripped)
description: |
{{describe_appearance}}
These are instructions TO the LLM. They guide generation but don't appear in output.
Concrete Comments (Preserved)
description: |
{{describe_appearance}}
sims_traits:
{{generate_traits}}
These are comments FOR the output file. They explain the generated content to future readers.
How the LLM Knows
| Indicator | Type | Action |
|---|
# TEMPLATE:, # INSTRUCTION:, # NOTE: | Meta | Strip |
# TODO:, # FIXME: in template context | Meta | Strip |
# This explains..., # See also... | Concrete | Preserve |
Comments inside {{...}} blocks | Meta | Strip |
| Comments explaining generated values | Concrete | Preserve |
| ALL CAPS directive style | Meta | Strip |
| Lowercase explanatory style | Concrete | Preserve |
Example: Mixed Comments
Template:
id: {{generate_id}}
name: "{{character_name}}"
description: |
{{describe_appearance}}
sims_traits:
nice: {{nice_value}}
playful: {{infer_playful}}
Generated Output:
id: palm-001
name: "Palm"
description: |
A small capuchin monkey with knowing eyes and surprisingly
dexterous fingers. Palm moves with the confidence of someone
who has already seen how this story ends.
sims_traits:
nice: 7
playful: 9
What happened:
# TEMPLATE: and # INSTRUCTION: lines → stripped
# This character was incarnated... → preserved
# Personality traits affect... → preserved
# 0-10, affects social success → preserved
# INSTRUCTION: Infer from description → stripped
The Principle
Meta-comments teach the generator. Concrete comments teach the reader.
The LLM understands this distinction because it understands intent. Directive language instructs; explanatory language documents.
Relationship to Self-Style Inheritance
Templates are prototypes. Instantiation creates instances:
prototype: skills/character/CHARACTER.yml.tmpl
↓ instantiate with context
instance: examples/adventure-4/characters/animals/monkey-palm/CHARACTER.yml
The template defines shape and intent. The instance contains specific values.
But empathic templates go further: they generate appropriate content, not just fill slots.
Examples
Instantiating a Buff
buff:
id: {{buff_id}}
name: "{{buff_name}}"
type: {{buff_type}}
description: |
{{describe_buff_effect_flavorfully}}
mechanics:
duration: {{duration}}
effect: "{{describe_mechanical_effect}}"
trigger: "{{empathic_expression: trigger_condition}}"
flavor_text: |
{{generate_flavor_text_matching_setting}}
Context:
buff_name: "Sugar Rush"
buff_type: positive
trigger_condition: "eating candy or sweet treats"
setting: "whimsical candy land"
Generated:
buff:
id: sugar-rush
name: "Sugar Rush"
type: positive
description: |
Your eyes widen, your heart races, and suddenly
EVERYTHING IS AMAZING. Colors are brighter, sounds
are sweeter, and you're pretty sure you could
outrun a unicorn.
mechanics:
duration: 10 minutes
effect: "+20% speed, +10% charisma, -10% focus"
trigger: "consuming_sweet_treat"
flavor_text: |
*The sugar hits your bloodstream like a
candy-coated lightning bolt. WHEEEEE!*
Templates as Schemas (CRITICAL!)
Templates are not just for instantiation — they ARE THE SCHEMA. The same .tmpl file serves as:
- Human documentation — What fields exist, what they mean
- Machine schema — Required vs optional, types, constraints
- Prototype definition — Default values, inherited structure
- Code generation source — Python dataclasses, JS classes, validators
The Template-Schema Pattern
Smart Instantiation: The Drop Pattern
Traditional: Fill every slot, even if the value is default.
Empathic: DROP optional sections entirely if:
- The value equals the prototype default
- The context doesn't need it
- An abstract description is sufficient
parameters:
time:
advancement: normal
git:
auto_commit: false
auto_push: false
debug:
enabled: false
parameters:
debug:
enabled: true
Schema Markers in Templates
Use special comments to mark field requirements:
adventure:
name: "{{adventure_name}}"
objective: "{{quest_objective}}"
status: active
evidence:
clues: []
items: []
statistics:
rooms_explored: 0
turns_elapsed: 0
parameters:
Field Requirement Markers
| Marker | Meaning | Instantiation Behavior |
|---|
# REQUIRED | Must be filled | Error if missing |
# OPTIONAL | Can be omitted | Drop if default or empty |
# OPTIONAL: default X | Has default value | Drop if equals X |
# COMPUTED | System generates | Never fill from context |
# INHERITED | From prototype | Drop if unchanged |
# ABSTRACT | Natural language OK | Keep as prose placeholder |
Abstract Fields: Natural Language as Value
Sometimes the "value" is just a description of intent:
room:
atmosphere: "{{atmosphere}}"
room:
atmosphere: |
A sense of ancient mystery. Dust motes float in shafts of light
from high windows. Something important happened here, long ago.
Prototype Inheritance
Templates inherit from prototypes. Only override what differs:
room:
name: "Maze Entrance"
exits:
north: corridor-1/
east: dead-end/
atmosphere: "Confusion and possibility"
Code Generation from Templates
Templates inform Python and JavaScript class generation:
@dataclass
class Adventure:
name: str
objective: str
starting_room: str
player_character: str
status: str = "active"
parameters: Optional[Parameters] = None
started: str = field(init=False)
turns_elapsed: int = field(init=False, default=0)
_extra: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self):
self.started = datetime.now().isoformat()
class Adventure {
name;
objective;
status = "active";
parameters = null;
get started() { return this._started; }
get turns_elapsed() { return this._turns; }
constructor(data) {
if (!data.name) throw new Error("Adventure requires name");
if (!data.objective) throw new Error("Adventure requires objective");
Object.assign(this, data);
this._extra = {};
for (const [k, v] of Object.entries(data)) {
if (!this.constructor.KNOWN_FIELDS.includes(k)) {
this._extra[k] = v;
}
}
}
}
LLM Compilation Events
When the template contains expressions, emit events for the LLM to compile:
guard:
allows_entry: "{{empathic_expression: player has the key OR player is known to guard}}"
greeting: "{{generate: appropriate greeting based on player reputation}}"
actions:
- trigger: "{{when: player attempts to pass without permission}}"
action: "{{do: block and challenge}}"
score: "{{calculate: based on guard alertness and player stealth}}"
The adventure.py linter emits events:
- event: COMPILE_EXPRESSION
field: "guard.allows_entry"
source: "player has the key OR player is known to guard"
target_language: javascript
expected_type: boolean
output_field: "guard.allows_entry_js"
- event: COMPILE_GENERATION
field: "guard.greeting"
instruction: "appropriate greeting based on player reputation"
target_format: string
context_needed: [player.reputation, guard.personality]
- event: COMPILE_SCORE
field: "guard.actions[0].score"
instruction: "based on guard alertness and player stealth"
target_format: "number 0-100"
inputs: [guard.alertness, player.stealth]
The LLM responds with compiled expressions, written to the _js or _py suffixed fields:
guard:
allows_entry: "player has the key OR player is known to guard"
allows_entry_js: |
(ctx) => ctx.player.inventory.includes('key') ||
ctx.guard.knownPlayers.includes(ctx.player.id)
allows_entry_py: |
lambda ctx: 'key' in ctx.player.inventory or
ctx.player.id in ctx.guard.known_players
greeting: "appropriate greeting based on player reputation"
greeting_js: |
(ctx) => {
if (ctx.player.reputation > 80) return "Welcome back, friend!";
if (ctx.player.reputation > 50) return "You may pass.";
return "Halt! State your business.";
}
Output Field Naming Convention
| Field | Output Field (JS) | Output Field (Py) |
|---|
allows_entry | allows_entry_js | allows_entry_py |
score | score_js | score_py |
guard.condition | guard.condition_js | guard.condition_py |
The runtime classes know to look for these suffixed fields:
class Guard {
checkEntry(ctx) {
if (this.allows_entry_js) {
return eval(this.allows_entry_js)(ctx);
}
return this.evaluateEmpathic(this.allows_entry, ctx);
}
}
class Guard:
def check_entry(self, ctx):
if self.allows_entry_py:
return eval(self.allows_entry_py)(ctx)
return self.evaluate_empathic(self.allows_entry, ctx)
This creates a graceful degradation pattern:
- If
_js or _py exists → use fast compiled expression
- If not → fall back to LLM interpretation of natural language
The same YAML file works in both compiled (fast) and interpreted (flexible) modes!
The Full Template-to-Code Pipeline
template_pipeline:
- stage: "ADVENTURE.yml.tmpl"
role: "Human-readable template/schema"
- stage: "adventure.py lint"
role: "Parse, validate, emit events"
actions:
- "Validate REQUIRED fields"
- "Warn on missing OPTIONAL with no default"
- "Emit COMPILE_EXPRESSION events"
- "Emit COMPILE_GENERATION events"
- "Emit COMPILE_SCORE events"
- stage: "LLM receives events"
role: "Compiles expressions"
- stage: "adventure.py compile"
role: "Generate output"
outputs:
- "adventure.json (minimal, machine-readable)"
- "engine.js (with compiled expressions)"
- "schema.py (Python classes for validation)"
- "index.html (playable web app)"
Validation and Linting
Templates enable automated validation:
validation:
required_present:
- adventure.name
- adventure.objective
- player.character
- navigation.starting_room
type_checks:
- field: simulation.turn
type: integer
min: 0
- field: parameters.difficulty
type: enum
values: [easy, normal, hard]
expression_checks:
- field: guard.allows_entry
must_compile_to: boolean
path_checks:
- field: player.character
must_exist: true
must_be_type: directory
Example: Smart Instantiation
Template (ROOM.yml.tmpl):
room:
name: "{{name}}"
purpose: "{{purpose}}"
created: "{{timestamp}}"
context: []
cards_in_play: []
working_set:
- "ROOM.yml"
- "README.md"
- "state/*.yml"
exits:
parent: "../"
atmosphere: "{{atmosphere}}"
Minimal valid instantiation:
room:
name: "Wizard's Study"
exits:
parent: "../"
down: secret-lab/
atmosphere: "Dusty tomes and arcane instruments"
What the linter infers:
- ✅
name provided (REQUIRED)
- ✅
exits has at least one (REQUIRED)
- ⚡
purpose missing → use default "general"
- ⚡
context missing → use default []
- ⚡
cards_in_play missing → use default []
- ⚡
working_set missing → inherit from prototype
- ⚡
created missing → compute timestamp
- ✅
atmosphere is natural language (ABSTRACT valid)
Anti-Pattern: Dumb Templates
Don't do this:
description: "{{description}}"
personality: "{{personality}}"
Do this:
description: |
{{describe_character_based_on_traits_and_setting}}
personality:
{{infer_traits_from_context}}
Let the LLM add value. That's the whole point.
Anti-Pattern: Repeating Defaults
Don't do this:
working_set:
- "ROOM.yml"
- "README.md"
- "state/*.yml"
Do this:
Templates enable inheritance. Use it!
Dovetails With
Protocol Symbol
EMPATHIC-TEMPLATES
Invoke when: Instantiating templates with semantic understanding.
See: PROTOCOLS.yml