first commit
This commit is contained in:
530
api/docs/PROMPT_TEMPLATES.md
Normal file
530
api/docs/PROMPT_TEMPLATES.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# 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")
|
||||
```
|
||||
Reference in New Issue
Block a user