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