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

13 KiB

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

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

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:

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:

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:

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:

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.

{{ items | format_inventory }}
{# Output: "Health Potion (x3), Sword, Shield, and 5 more items" #}

Parameters: max_items (default 10)

format_stats

Formats stat dictionaries.

{{ character.stats | format_stats }}
{# Output: "Strength: 16, Dexterity: 14, Constitution: 15" #}

format_skills

Formats skill lists with levels.

{{ 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.

{{ character.effects | format_effects }}
{# Output: "Blessed (3 turns), Poisoned (2 turns)" #}

truncate_text

Truncates long text with ellipsis.

{{ 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.

{{ 10000 | format_gold }}
{# Output: "10,000 gold" #}

Global Functions

Available in all templates:

{{ 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:

{#
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

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:

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:

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

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

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