Files
Code_of_Conquest/api/docs/PROMPT_TEMPLATES.md
2025-11-24 23:10:55 -06:00

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")
```