531 lines
13 KiB
Markdown
531 lines
13 KiB
Markdown
# Prompt Templates Documentation
|
|
|
|
## Overview
|
|
|
|
The prompt template system uses Jinja2 to build consistent, well-structured prompts for AI generation. Templates separate prompt logic from application code, making prompts easy to iterate and maintain.
|
|
|
|
**Location:** `app/ai/prompt_templates.py` and `app/ai/templates/*.j2`
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
PromptTemplates (singleton)
|
|
├── Jinja2 Environment
|
|
│ ├── FileSystemLoader (templates directory)
|
|
│ ├── Custom filters (format_inventory, etc.)
|
|
│ └── Global functions (len, min, max)
|
|
└── Templates
|
|
├── story_action.j2
|
|
├── combat_action.j2
|
|
├── quest_offering.j2
|
|
└── npc_dialogue.j2
|
|
```
|
|
|
|
---
|
|
|
|
## Basic Usage
|
|
|
|
### Quick Start
|
|
|
|
```python
|
|
from app.ai.prompt_templates import render_prompt
|
|
|
|
# Render a template with context
|
|
prompt = render_prompt(
|
|
"story_action.j2",
|
|
character={"name": "Aldric", "level": 3, "player_class": "Fighter", ...},
|
|
action="I search for hidden doors",
|
|
game_state={"current_location": "Ancient Library", ...}
|
|
)
|
|
```
|
|
|
|
### Using the PromptTemplates Class
|
|
|
|
```python
|
|
from app.ai.prompt_templates import get_prompt_templates, PromptTemplates
|
|
|
|
# Get singleton instance
|
|
templates = get_prompt_templates()
|
|
|
|
# Or create custom instance with different directory
|
|
templates = PromptTemplates(template_dir="/path/to/custom/templates")
|
|
|
|
# Render template
|
|
prompt = templates.render("story_action.j2", **context)
|
|
|
|
# Render from string
|
|
prompt = templates.render_string(
|
|
"Hello {{ name }}, you are level {{ level }}",
|
|
name="Aldric",
|
|
level=3
|
|
)
|
|
|
|
# List available templates
|
|
available = templates.get_template_names()
|
|
# ['story_action.j2', 'combat_action.j2', ...]
|
|
```
|
|
|
|
---
|
|
|
|
## Available Templates
|
|
|
|
### 1. Story Action (`story_action.j2`)
|
|
|
|
Generates DM responses to player story actions.
|
|
|
|
**Required Context:**
|
|
- `character` - Character dict with name, level, player_class, stats, current_hp, max_hp
|
|
- `game_state` - Dict with current_location, location_type, active_quests
|
|
- `action` - String describing the player's action
|
|
|
|
**Optional Context:**
|
|
- `conversation_history` - List of recent turns (turn, action, dm_response)
|
|
- `world_context` - Additional world information
|
|
- `action_instructions` - Action-specific AI instructions from `dm_prompt_template` in action_prompts.yaml
|
|
- `max_tokens` - Token limit for response length guidance
|
|
|
|
**Player Agency Rules:**
|
|
|
|
The template includes critical rules to ensure AI respects player choice:
|
|
- Never make decisions for the player (no auto-purchasing)
|
|
- Never complete transactions without consent
|
|
- Present options and ask what they want to do
|
|
- If items have costs, show prices and ask to proceed
|
|
|
|
**Example:**
|
|
```python
|
|
prompt = render_prompt(
|
|
"story_action.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 3,
|
|
"player_class": "Fighter",
|
|
"current_hp": 25,
|
|
"max_hp": 30,
|
|
"stats": {"strength": 16, "dexterity": 14, "constitution": 15},
|
|
"skills": [{"name": "Athletics", "level": 2}],
|
|
"effects": [{"name": "Blessed", "remaining_turns": 3}]
|
|
},
|
|
game_state={
|
|
"current_location": "Ancient Library",
|
|
"location_type": "DUNGEON",
|
|
"active_quests": ["find_artifact"],
|
|
"discovered_locations": ["Village", "Forest"],
|
|
"time_of_day": "Evening"
|
|
},
|
|
action="I search the room for hidden doors",
|
|
conversation_history=[
|
|
{"turn": 1, "action": "entered library", "dm_response": "You push open the heavy oak doors..."},
|
|
{"turn": 2, "action": "examined shelves", "dm_response": "The shelves contain dusty tomes..."}
|
|
]
|
|
)
|
|
```
|
|
|
|
**Output Structure:**
|
|
- Character status (HP, stats, skills, effects)
|
|
- Current situation (location, quests, time)
|
|
- Recent history (last 3 turns)
|
|
- Player action
|
|
- Action-specific instructions (if provided)
|
|
- Generation task with length guidance
|
|
- Player agency rules
|
|
|
|
---
|
|
|
|
### 2. Combat Action (`combat_action.j2`)
|
|
|
|
Narrates combat actions with dramatic flair.
|
|
|
|
**Required Context:**
|
|
- `character` - Character dict
|
|
- `combat_state` - Dict with enemies, round_number, current_turn
|
|
- `action` - Combat action description
|
|
- `action_result` - Dict with hit, damage, effects_applied, target
|
|
|
|
**Optional Context:**
|
|
- `is_critical` - Boolean for critical hits
|
|
- `is_finishing_blow` - Boolean if enemy is defeated
|
|
|
|
**Example:**
|
|
```python
|
|
prompt = render_prompt(
|
|
"combat_action.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 3,
|
|
"player_class": "Fighter",
|
|
"current_hp": 20,
|
|
"max_hp": 30,
|
|
"effects": []
|
|
},
|
|
combat_state={
|
|
"round_number": 3,
|
|
"current_turn": "Aldric",
|
|
"enemies": [
|
|
{"name": "Goblin Chief", "current_hp": 8, "max_hp": 25, "effects": []}
|
|
]
|
|
},
|
|
action="swings their longsword at the Goblin Chief",
|
|
action_result={
|
|
"hit": True,
|
|
"damage": 12,
|
|
"effects_applied": ["bleeding"],
|
|
"target": "Goblin Chief"
|
|
},
|
|
is_critical=True,
|
|
is_finishing_blow=False
|
|
)
|
|
```
|
|
|
|
**Output Structure:**
|
|
- Combatants (player and enemies with HP/effects)
|
|
- Combat round and turn
|
|
- Action and result
|
|
- Narrative instructions (1-2 paragraphs)
|
|
|
|
---
|
|
|
|
### 3. Quest Offering (`quest_offering.j2`)
|
|
|
|
AI selects the most contextually appropriate quest.
|
|
|
|
**Required Context:**
|
|
- `character` - Character dict with completed_quests
|
|
- `eligible_quests` - List of quest dicts
|
|
- `game_context` - Dict with current_location, location_type, active_quests
|
|
|
|
**Optional Context:**
|
|
- `recent_actions` - List of recent player action strings
|
|
|
|
**Example:**
|
|
```python
|
|
prompt = render_prompt(
|
|
"quest_offering.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 3,
|
|
"player_class": "Fighter",
|
|
"completed_quests": ["tutorial_quest"]
|
|
},
|
|
eligible_quests=[
|
|
{
|
|
"quest_id": "goblin_cave",
|
|
"name": "Clear the Goblin Cave",
|
|
"difficulty": "EASY",
|
|
"quest_giver": "Village Elder",
|
|
"description": "A nearby cave has been overrun by goblins...",
|
|
"narrative_hooks": [
|
|
"The village elder looks worried about recent goblin attacks",
|
|
"Farmers complain about lost livestock"
|
|
]
|
|
},
|
|
{
|
|
"quest_id": "herb_gathering",
|
|
"name": "Gather Healing Herbs",
|
|
"difficulty": "EASY",
|
|
"quest_giver": "Herbalist",
|
|
"description": "The local herbalist needs rare herbs...",
|
|
"narrative_hooks": [
|
|
"The herbalist mentions a shortage of supplies",
|
|
"You notice the apothecary shelves are nearly bare"
|
|
]
|
|
}
|
|
],
|
|
game_context={
|
|
"current_location": "The Rusty Anchor Tavern",
|
|
"location_type": "TAVERN",
|
|
"active_quests": [],
|
|
"world_events": ["goblin raids increasing"]
|
|
},
|
|
recent_actions=["asked about rumors", "talked to locals"]
|
|
)
|
|
```
|
|
|
|
**Output:** Just the quest_id (e.g., `goblin_cave`)
|
|
|
|
---
|
|
|
|
### 4. NPC Dialogue (`npc_dialogue.j2`)
|
|
|
|
Generates contextual NPC conversations.
|
|
|
|
**Required Context:**
|
|
- `character` - Player character dict
|
|
- `npc` - NPC dict with name, role, personality
|
|
- `conversation_topic` - What the player wants to discuss
|
|
- `game_state` - Current game state
|
|
|
|
**Optional Context:**
|
|
- `npc_relationship` - Description of relationship
|
|
- `previous_dialogue` - List of exchanges (player_line, npc_response)
|
|
- `npc_knowledge` - List of things this NPC knows
|
|
|
|
**Example:**
|
|
```python
|
|
prompt = render_prompt(
|
|
"npc_dialogue.j2",
|
|
character={
|
|
"name": "Aldric",
|
|
"level": 3,
|
|
"player_class": "Fighter"
|
|
},
|
|
npc={
|
|
"name": "Old Barkeep",
|
|
"role": "Tavern Owner",
|
|
"personality": "Gruff but kind-hearted",
|
|
"speaking_style": "Short sentences, occasional grunt",
|
|
"goals": "Keep the tavern running, protect the village",
|
|
"secret_knowledge": "Knows about the hidden cellar entrance"
|
|
},
|
|
conversation_topic="What rumors have you heard lately?",
|
|
game_state={
|
|
"current_location": "The Rusty Anchor",
|
|
"time_of_day": "Evening",
|
|
"active_quests": []
|
|
},
|
|
npc_relationship="Acquaintance - met twice before",
|
|
npc_knowledge=["goblin attacks", "missing merchant", "ancient ruins"],
|
|
previous_dialogue=[
|
|
{
|
|
"player_line": "I'll have an ale",
|
|
"npc_response": "*slides a mug across the bar* Two copper. You new around here?"
|
|
}
|
|
]
|
|
)
|
|
```
|
|
|
|
**Output:** NPC dialogue with action/emotion tags
|
|
Format: `*action* "Dialogue here."`
|
|
|
|
---
|
|
|
|
## Custom Filters
|
|
|
|
Templates have access to these formatting filters:
|
|
|
|
### `format_inventory`
|
|
|
|
Formats item lists with quantities.
|
|
|
|
```jinja2
|
|
{{ items | format_inventory }}
|
|
{# Output: "Health Potion (x3), Sword, Shield, and 5 more items" #}
|
|
```
|
|
|
|
**Parameters:** `max_items` (default 10)
|
|
|
|
### `format_stats`
|
|
|
|
Formats stat dictionaries.
|
|
|
|
```jinja2
|
|
{{ character.stats | format_stats }}
|
|
{# Output: "Strength: 16, Dexterity: 14, Constitution: 15" #}
|
|
```
|
|
|
|
### `format_skills`
|
|
|
|
Formats skill lists with levels.
|
|
|
|
```jinja2
|
|
{{ character.skills | format_skills }}
|
|
{# Output: "Athletics (Lv.2), Perception (Lv.3), and 2 more skills" #}
|
|
```
|
|
|
|
**Parameters:** `max_skills` (default 5)
|
|
|
|
### `format_effects`
|
|
|
|
Formats active effects/buffs/debuffs.
|
|
|
|
```jinja2
|
|
{{ character.effects | format_effects }}
|
|
{# Output: "Blessed (3 turns), Poisoned (2 turns)" #}
|
|
```
|
|
|
|
### `truncate_text`
|
|
|
|
Truncates long text with ellipsis.
|
|
|
|
```jinja2
|
|
{{ long_description | truncate_text(100) }}
|
|
{# Output: "This is a very long description that will be cut off..." #}
|
|
```
|
|
|
|
**Parameters:** `max_length` (default 100)
|
|
|
|
### `format_gold`
|
|
|
|
Formats currency with commas.
|
|
|
|
```jinja2
|
|
{{ 10000 | format_gold }}
|
|
{# Output: "10,000 gold" #}
|
|
```
|
|
|
|
---
|
|
|
|
## Global Functions
|
|
|
|
Available in all templates:
|
|
|
|
```jinja2
|
|
{{ len(items) }} {# Length of list #}
|
|
{{ min(a, b) }} {# Minimum value #}
|
|
{{ max(a, b) }} {# Maximum value #}
|
|
{% for i, item in enumerate(items) %}
|
|
{{ i }}: {{ item }}
|
|
{% endfor %}
|
|
```
|
|
|
|
---
|
|
|
|
## Creating New Templates
|
|
|
|
### Template Structure
|
|
|
|
1. Create file in `app/ai/templates/` with `.j2` extension
|
|
2. Add documentation header with required/optional context
|
|
3. Structure the prompt with clear sections
|
|
4. End with task instructions
|
|
|
|
**Template Example:**
|
|
```jinja2
|
|
{#
|
|
My Custom Template
|
|
Description of what this template does.
|
|
|
|
Required context:
|
|
- param1: Description
|
|
- param2: Description
|
|
|
|
Optional context:
|
|
- param3: Description
|
|
#}
|
|
|
|
## Section Header
|
|
Content using {{ param1 }}
|
|
|
|
{% if param3 %}
|
|
## Optional Section
|
|
{{ param3 }}
|
|
{% endif %}
|
|
|
|
## Task Instructions
|
|
Tell the AI what to do...
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
1. **Clear documentation** - Always document required/optional context
|
|
2. **Structured output** - Use markdown headers for readability
|
|
3. **Graceful fallbacks** - Use `{% if %}` for optional fields
|
|
4. **Concise prompts** - Include only necessary context
|
|
5. **Explicit instructions** - Tell AI exactly what to output
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
```python
|
|
from app.ai.prompt_templates import PromptTemplateError
|
|
|
|
try:
|
|
prompt = render_prompt("unknown.j2", **context)
|
|
except PromptTemplateError as e:
|
|
logger.error(f"Template error: {e}")
|
|
# Handle missing template or rendering error
|
|
```
|
|
|
|
Common errors:
|
|
- Template not found
|
|
- Missing required context variables
|
|
- Filter errors (e.g., None passed to filter)
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Template Directory
|
|
|
|
Default: `app/ai/templates/`
|
|
|
|
Custom directory:
|
|
```python
|
|
templates = PromptTemplates(template_dir="/custom/path")
|
|
```
|
|
|
|
### Jinja2 Settings
|
|
|
|
- `trim_blocks=True` - Remove newline after block tags
|
|
- `lstrip_blocks=True` - Remove leading whitespace before block tags
|
|
- `autoescape=True` - Escape HTML/XML (security)
|
|
|
|
---
|
|
|
|
## Integration with NarrativeGenerator
|
|
|
|
The `NarrativeGenerator` uses templates internally:
|
|
|
|
```python
|
|
from app.ai.narrative_generator import NarrativeGenerator
|
|
|
|
generator = NarrativeGenerator()
|
|
|
|
# Uses story_action.j2 internally
|
|
response = generator.generate_story_response(
|
|
character=character,
|
|
action=action,
|
|
game_state=game_state,
|
|
user_tier=UserTier.PREMIUM
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Templates
|
|
|
|
### Unit Test Pattern
|
|
|
|
```python
|
|
from app.ai.prompt_templates import PromptTemplates
|
|
|
|
def test_story_action_template():
|
|
templates = PromptTemplates()
|
|
|
|
context = {
|
|
"character": {
|
|
"name": "Test",
|
|
"level": 1,
|
|
"player_class": "Fighter",
|
|
"current_hp": 10,
|
|
"max_hp": 10,
|
|
"stats": {"strength": 10}
|
|
},
|
|
"game_state": {
|
|
"current_location": "Test Location",
|
|
"location_type": "TOWN"
|
|
},
|
|
"action": "Test action"
|
|
}
|
|
|
|
prompt = templates.render("story_action.j2", **context)
|
|
|
|
assert "Test" in prompt
|
|
assert "Fighter" in prompt
|
|
assert "Test action" in prompt
|
|
```
|
|
|
|
### Manual Testing
|
|
|
|
```python
|
|
from app.ai.prompt_templates import render_prompt
|
|
|
|
# Render and print to inspect
|
|
prompt = render_prompt("story_action.j2", **test_context)
|
|
print(prompt)
|
|
print(f"\nLength: {len(prompt)} chars")
|
|
```
|