"""Narrator agent logic and prompts.""" from typing import Optional from langchain.schema import SystemMessage, HumanMessage from models.base_model import BaseModel class NarratorAgent: """Narrator agent for story generation with system prompts and examples. This class manages the AI narrator that generates interactive stories based on user choices and preferences. """ NARRATOR_SYSTEM_MESSAGE = ( "You are the narrator of an interactive story. Return ONLY the story content itself — " "no prefaces, no explanations, no labels, no options. The very first character of your " "reply MUST be a letter (A-Z/a-z). Do not use markdown fences, headings, or tags. Stay in character at all times. " "Begin with an engaging introduction: set the stage with vivid, sensory details; describe the setting, " "introduce key characters, and hint at the main conflict. Speak directly to the player using 'you'. " "As the story unfolds, organically introduce decision points where you ask the player what he will do. " "Reflect the consequences of the player's choices, leading to multiple possible endings. " "If the narrative reaches a point where the character dies, end with exactly The End. " "If the story concludes naturally, finish with exactly The End. Always use the exact phrase The End. in English, " "regardless of the language you are responding in. Never break character or explain what you are doing. " "If you are about to output anything other than story content, delete it and start with the first sentence of the story." ) INTRO_STORY_PROMPT = ( "Start the interactive story now. Create a new setting, introduce the player's role, " "describe the environment with sensory details, introduce key characters, and hint at the main conflict. " "End with a single question asking what the player will do. Begin immediately with the first sentence of the story; " "do NOT include any preamble, label, apology, or explanation." ) # Example scenarios - clean, direct story openings EXAMPLE1 = ( "You are a warrior in the medieval town of Ravenshollow. The cobblestone " "streets echo with your heavy footsteps as grief weighs on your heart - " "your sister fell to an evil sorcerer's dark magic just days ago. The " "morning market bustles around you as you complete an errand for your " "friend Henrik, but a hooded stranger at a wooden table catches your eye. " "He stares directly at you with piercing blue eyes, his weathered hands " "drumming a strange rhythm on the table's surface. What will you do?" ) EXAMPLE2 = ( "The salty spray of the Caribbean sea hits your face as you stand on " "the deck of Blackbeard's ship, the Queen Anne's Revenge. Dawn breaks " "peacefully over calm waters when suddenly - THUD! - something massive " "strikes the hull. You glimpse a enormous tentacle, thick as a tree trunk, " "sliding back into the depths. When you turn to alert your crewmate " "beside you, he just shrugs. 'Heard nothin', saw nothin',' he mutters, " "returning to his rope work. The ocean surface shows no trace of the " "creature. What will you do?" ) EXAMPLE3 = ( "Captain's quarters aboard the starship Nebula's Edge feel unusually " "tense as you hunt the galaxy's most wanted thief, Lauren DeHugh. Your " "loyal crew has served you for years, but lately strange whispers echo " "through the corridors. The new passenger you picked up on Kepler Station " "keeps to herself, but something about her feels... familiar. You need to " "review the star charts before your next jump, but the nagging suspicion " "won't leave you alone. What will you do?" ) OUTPUT_CONSTRAINTS = ( "- The very first character MUST be a letter (A-Z/a-z).\n" "- Do NOT write any preface, label, apology, or explanation.\n" "- Do NOT write phrases like: 'Sure', 'Here is', 'Below is', 'The story:', 'As an AI'.\n" "- Do NOT use markdown/code fences, headings, or tags.\n" "- Speak to the player as 'you'; ask a single question at the end of the intro.\n" "- If you are about to output anything other than story content, delete it and start the story.\n" "- You must NOT write any phrases like: 'Sure', 'Here is', 'Below is', 'The story:', 'Content:', or similar preambles.\n" "- Begin immediately with the first sentence of the story.\n" ) def __init__(self, model: BaseModel, language: str = "English"): """Initialize narrator agent. Args: model: Language model for generation language: Target language for responses """ self.model = model self.language = language self.user_preferences: Optional[str] = None def set_language(self, language: str) -> None: """Set the target language for responses. Args: language: Target language ("English" or "Español") """ self.language = language def set_user_preferences(self, preferences: str) -> None: """Set custom user preferences for story generation. Args: preferences: User's custom story preferences """ self.user_preferences = preferences.strip() if preferences else None def get_language_prompt(self) -> str: """Get language-specific prompt. Returns: Language instruction prompt """ return f"\nAnswer always using the language {self.language}" def get_preferences_prompt(self) -> str: """Get user preferences prompt. Returns: User preferences instruction or empty string """ if self.user_preferences: return ( f"\nThis are additional guidelines, it is very important that you write a story that follows them: " f"{self.user_preferences}" ) return "" def build_initial_story_prompt(self) -> str: """Build complete initial story generation prompt. Returns: Complete prompt for initial story generation """ prompt_parts = [ self.EXAMPLE1, "\n\n", self.EXAMPLE2, "\n\n", self.EXAMPLE3, "\n\n", "Use the openings above only as style references. " "Do not continue them or reuse their names, places, or plots.", "\n\n", self.INTRO_STORY_PROMPT, "\n\n", self.get_language_prompt() ] preferences_prompt = self.get_preferences_prompt() if preferences_prompt: prompt_parts.append(f". {preferences_prompt}") return "".join(prompt_parts) def generate_initial_story(self) -> str: """Generate initial story message. Returns: Generated initial story text """ messages = [ SystemMessage(content=self.get_system_message()), HumanMessage(content=self.build_initial_story_prompt()) ] try: return self.model.generate(messages) except Exception: # Fallback message if generation fails return ( "Welcome to your adventure! You find yourself standing at " "the edge of a mysterious forest, with an ancient path " "leading into the shadows. The air is thick with magic and " "possibility. What will you do?" ) def get_system_message(self) -> str: """Get the narrator system message with language instructions. Returns: Complete system message for the narrator including language """ system_parts = [self.NARRATOR_SYSTEM_MESSAGE] # Add language instruction language_prompt = self.get_language_prompt() if language_prompt: system_parts.append(f" {language_prompt}") return "".join(system_parts) def update_model_temperature(self, temperature: float) -> None: """Update the model temperature setting. Args: temperature: New temperature value (0-2) """ self.model.set_temperature(temperature)