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