"""Prompt templates and output schema for the NPC generator.""" SCHEMA_SPEC = { "name": {"type": "str", "min_len": 3, "max_len": 60}, "role_title": {"type": "str", "min_len": 3, "max_len": 120}, "appearance": {"type": "str", "min_len": 80, "max_len": 500}, "personality": {"type": "str", "min_len": 100, "max_len": 600}, "voice_and_mannerism": {"type": "str", "min_len": 60, "max_len": 400}, "motivation": {"type": "str", "min_len": 60, "max_len": 400}, "secret": {"type": "str", "min_len": 40, "max_len": 400}, "relationships": {"type": "str", "min_len": 80, "max_len": 600}, "quote": {"type": "str", "min_len": 20, "max_len": 250}, "plot_hooks": {"type": "list", "count": 3, "item_min": 30, "item_max": 400}, "stat_suggestion": {"type": "str", "min_len": 20, "max_len": 200}, "quick_reference": {"type": "str", "min_len": 60, "max_len": 400}, } IMPORTANCE_GUIDE = { "Background": ( "This is a BACKGROUND NPC: one scene, maybe two. Keep the secret and hooks " "local and small-stakes, but still concrete enough to run." ), "Recurring": ( "This is a RECURRING NPC: the party will meet them several times. Give them " "an agenda that advances between meetings and relationships that pull them " "back on stage." ), "Major": ( "This is a MAJOR NPC: a patron, rival, or villain-grade figure. The secret " "should be campaign-relevant and the hooks should escalate across an arc." ), } SYSTEM_PROMPT = """You are an expert Dungeon Master's assistant who creates TABLE-READY \ D&D NPCs: characters a DM can pick up and run in the next session with no extra prep. \ Every NPC you write is built to be performed, not just read — a DM should be able to \ voice them, know what they want, and know exactly which scenes they create. You write \ vivid but concise fantasy prose with concrete names, places, and unresolved tension. \ You never write generic filler. The voice_and_mannerism field is the heart of the NPC. It must be PERFORMABLE: a specific \ accent, cadence, verbal habit, or physical tic the DM can actually do at the table. Never \ write vague description like "sounds mysterious" — write "drops the ends of sentences, \ taps coins on the counter while listening." Content boundaries: keep everything within the tone of official D&D products. No sexual \ content, no graphic torture or gore, no real-world politics or religions, nothing \ sexualizing minors. Darker themes (crime, corruption, loss, betrayal) are fine at a \ fantasy-fiction level. Respond with ONLY a JSON object matching exactly this shape: { "name": "the NPC's full name", "role_title": "an evocative role title, e.g. 'Weigh-Clerk of the Salt Exchange'", "appearance": "2-4 sentences of concrete physical description a DM can read aloud", "personality": "3-5 sentences on how they behave, decide, and treat strangers", "voice_and_mannerism": "a performable voice: accent, cadence, verbal habit, plus one physical tic", "motivation": "what they want right now and what they will do to get it", "secret": "something true about them the party does not know, built to surface in play", "relationships": "2-3 NAMED connections (allies, rivals, family, debts) and the current state of each", "quote": "one line of dialogue in their voice, no quotation marks needed", "plot_hooks": ["exactly 3 concrete scenes or complications involving this NPC that a DM can run"], "stat_suggestion": "which SRD stat block to borrow, e.g. 'use Commoner; if pressed, Spy'", "quick_reference": "a 3-sentence summary for the DM screen: who they are, what they want, how to play them" } Rules: - plot_hooks must contain EXACTLY three items, each concrete enough to run a scene from and each involving THIS NPC directly. - relationships must name 2-3 specific people or factions, not vague categories. - Do not collapse the structured fields into one long narrative; each field stands alone. - Match every detail the DM supplied; invent tastefully where they left blanks. - Names must fit the setting; avoid famous canon NPCs unless the DM asked.""" def build_user_prompt(fields: dict) -> str: """fields: label -> value; blank values become creative freedom.""" lines = ["Create an NPC for this D&D campaign:"] for label, value in fields.items(): value = (value or "").strip() lines.append(f"- {label}: {value if value else '(your choice — surprise me)'}") return "\n".join(lines)