Game studios spend weeks writing NPC backstories, quest lines, and item descriptions. In this tutorial, I will show you how to build a Python pipeline that generates structured RPG content in JSON format using an LLM. The output imports directly into engines like Godot or Unity, and because Oxlo.ai charges a flat rate per request, you can feed it a 10,000 word world bible without watching your bill balloon.
What you'll need
- Python 3.10 or higher
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai
Step 1: Define the content schema
Game engines need predictable data. I start with a dataclass that mirrors the JSON structure we want from the model: an NPC, branching dialogue nodes, and a quest with objectives.
import json
from dataclasses import dataclass, asdict
from typing import List
@dataclass
class DialogueChoice:
text: str
next_id: str | None
@dataclass
class DialogueNode:
id: str
text: str
choices: List[DialogueChoice]
@dataclass
class Quest:
title: str
description: str
objectives: List[str]
reward_item: str
@dataclass
class NPC:
name: str
role: str
backstory: str
dialogue: List[DialogueNode]
quest: Quest
SCHEMA = """
{
"npc": {
"name": "string",
"role": "string",
"backstory": "string",
"dialogue": [
{
"id": "string",
"text": "string",
"choices": [
{"text": "string", "next_id": "string or null"}
]
}
],
"quest": {
"title": "string",
"description": "string",
"objectives": ["string"],
"reward_item": "string"
}
}
}
"""
Step 2: Craft the system prompt
The system prompt is the contract. It tells the model exactly what to generate and forbids markdown or extra commentary so we get clean JSON every time.
SYSTEM_PROMPT = f"""You are a senior narrative designer for a fantasy RPG.
Your job is to generate one complete NPC with a branching dialogue tree and a quest.
Follow these rules exactly:
1. Output ONLY valid JSON. No markdown, no commentary.
2. Adhere to this schema:
{SCHEMA}
3. The dialogue must contain at least 3 nodes with meaningful player choices.
4. The quest objectives must be concrete and measurable.
5. Fit the content to the theme provided by the user.
"""
Step 3: Wire up the Oxlo.ai client
Now I will create the generation function. I use the OpenAI SDK as a drop in replacement, pointing it at Oxlo.ai. For creative writing I like llama-3.3-70b, but you can swap in qwen-3-32b or kimi-k2.6 without changing any other code.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
def generate_npc(theme: str, model: str = "llama-3.3-70b"):
user_message = f"Theme: {theme}\nGenerate a complete NPC package."
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.7,
max_tokens=2048,
)
raw = response.choices[0].message.content
return json.loads(raw)
Step 4: Export to JSON
Generated content is useless if it stays in a terminal. This helper writes the parsed dict to a timestamped file so your engine can import it.
import os
from datetime import datetime
def save_npc(data: dict, theme: str, out_dir: str = "game_content"):
os.makedirs(out_dir, exist_ok=True)
safe_theme = theme.replace(" ", "_").replace(",", "").lower()
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
filename = f"{out_dir}/{safe_theme}_{timestamp}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return filename
Step 5: Batch generate a cast
Studios rarely need one NPC. This loop builds an entire cast from a shared world bible. Because Oxlo.ai uses flat per request pricing, you can stuff that bible into the system prompt on every call and still know exactly what the run will cost. See https://oxlo.ai/pricing for plan details.
def generate_cast(world_bible: str, themes: list, model: str = "llama-3.3-70b"):
results = []
enriched_system = SYSTEM_PROMPT + "\nWorld Bible:\n" + world_bible
for theme in themes:
user_message = f"Theme: {theme}\nGenerate a complete NPC package."
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": enriched_system},
{"role": "user", "content": user_message},
],
temperature=0.8,
max_tokens=2048,
)
raw = response.choices[0].message.content
npc_data = json.loads(raw)
path = save_npc(npc_data, theme)
results.append((npc_data["npc"]["name"], path))
print(f"Generated: {npc_data['npc']['name']} -> {path}")
return results
Run it
Here is a complete script that generates three characters for a sky city setting.
if __name__ == "__main__":
world_bible = """
The city of Aethelgard floats above a toxic sea.
Magic is powered by harvested lightning.
Guilds control the lightning towers.
"""
themes = [
"a disgraced lightning tower engineer",
"a black market lightning smuggler",
"a tower guard who hears voices in the static"
]
files = generate_cast(world_bible, themes, model="qwen-3-32b")
print(f"\nBatch complete. {len(files)} NPCs written to disk.")
Example terminal output:
Generated: Elara Vane -> game_content/a_disgraced_lightning_tower_engineer_20250601_143022.json
Generated: Kael Thren -> game_content/a_black_market_lightning_smuggler_20250601_143029.json
Generated: Mira Sol -> game_content/a_tower_guard_who_hears_voices_in_the_static_20250601_143035.json
Batch complete. 3 NPCs written to disk.
And the JSON file contains:
{
"npc": {
"name": "Elara Vane",
"role": "Disgraced Engineer",
"backstory": "Elara once maintained the North Spire until she rewired its grounding grid to save a residential district, violating guild protocol.",
"dialogue": [
{
"id": "greeting",
"text": "You look like someone who does not flinch from sparks. Good. I need a runner.",
"choices": [
{"text": "What do you need?", "next_id": "quest_offer"},
{"text": "I do not work for criminals.", "next_id": "hostile_end"}
]
},
{
"id": "quest_offer",
"text": "The guild vault has a crystal that could stabilize the lower district. Bring it to me.",
"choices": [
{"text": "I will do it.", "next_id": "accept"},
{"text": "That sounds dangerous.", "next_id": "negotiate"}
]
},
{
"id": "hostile_end",
"text": "Then get off my rooftop before I call the sparks down on you.",
"choices": []
}
],
"quest": {
"title": "Grounding the Storm",
"description": "Steal a replacement crystal from the guild vault and deliver it to Elara.",
"objectives": [
"Infiltrate the lower vault",
"Replace the tracking crystal",
"Escape without tripping the arc alarm"
],
"reward_item": "Insulated Grappling Hook"
}
}
}
Next steps
Hook this pipeline into your game engine's asset importer so writers can iterate on the world bible and regenerate the entire cast with one command. If you need longer context for massive world documents, switch the model to kimi-k2.6 and take advantage of its 131K context window on the same flat per request pricing.
Top comments (0)