Files
2026-09-21 22:20:24 +02:00

473 lines
14 KiB
Python

import json
import requests
from fastapi import FastAPI
from pydantic import BaseModel
from config import DEVICE, CHECKPOINT_DIR
from data.tokenizer import Tokenizer
from makescript import json_string_to_py
from model.builder_model import BuilderModel
from utils.checkpoint import latest_checkpoint, load_checkpoint
import subprocess
import sys
import os
import base64
app = FastAPI(title="Minecraft Builder AI")
if DEVICE == "cuda":
print("Cuda Mode: true")
else:
print("Cuda Mode: false")
print("Loading tokenizer...")
tokenizer = Tokenizer()
tokenizer.load("data")
print("Loading model...")
model = BuilderModel(tokenizer).to(DEVICE)
checkpoint = latest_checkpoint(CHECKPOINT_DIR)
load_checkpoint(
checkpoint,
model,
optimizer=None,
map_location=DEVICE,
)
model.eval()
class BuildRequest(BaseModel):
prompt: str
@app.post("/generate")
def generate(req: BuildRequest):
blocks, tokens = model.generate(req.prompt)
return {
"prompt": req.prompt,
"tokens": tokens,
"blocks": blocks,
}
class RunRequest(BaseModel):
script: str
@app.post("/run_script")
def runscript(req: RunRequest):
result = subprocess.run([
sys.executable,
"builders/" + req.script + ".py"
])
if result.returncode != 0:
return {
"success": False,
"schem": None
}
schem_path = "schematics/" + req.script + ".schem"
if not os.path.exists(schem_path):
return {
"success": False,
"schem": None
}
with open(schem_path, "rb") as f:
schem = base64.b64encode(f.read()).decode("ascii")
return {
"success": True,
"schem": schem
}
class AIScriptRequest(BaseModel):
url: str
model: str
api_key: str
prompt: str
context_length: int
temperature: float
reasoning: str
@app.post("/aiscript")
def aiscript(req: AIScriptRequest):
systemprompt = r"""
You are a Minecraft Building AI.
You build with the mcschematic python library.
Your task is to return a valid script in the following format:
{
"file_name": FILE_NAME,
"script: SCRIPT,
"response": RESPONSE
}
Rules:
- Only use valid Minecraft blocks
- The output folder is called "schematics"
- The schematic name must match with the FILE_NAME
- In settings there always needs to be a variable called "SEED"
Documentation:
mcschematic is a Python library for programmatically creating Minecraft Java Edition schematic files. The basic workflow is:
Create an MCSchematic object.
Place Minecraft blocks at (x, y, z) coordinates with setBlock().
Optionally manipulate blocks/entities/data.
Save the resulting structure as a .schem file with save().
The core conceptual model is:
import mcschematic
schematic = mcschematic.MCSchematic()
schematic.setBlock((x, y, z), "minecraft:oak_log")
schematic.setBlock((x, y, z), "minecraft:oak_leaves")
schematic.save(
"output_directory",
"my_structure",
mcschematic.Version.JE_1_20_1
)
Important concepts
MCSchematic
This is the main container representing the Minecraft structure.
schem = mcschematic.MCSchematic()
Think of it as an in-memory 3D voxel structure.
Coordinates
Blocks are addressed using:
(x, y, z)
For procedural generation, treat these as a normal 3D Cartesian coordinate system. Pick a clear origin convention—for example, (0, 0, 0) as the bottom-center of a tree—and build everything relative to that.
Placing blocks
The primary operation is:
schem.setBlock((x, y, z), "minecraft:oak_log")
Block IDs should use Minecraft's namespaced IDs:
minecraft:stone
minecraft:dirt
minecraft:oak_log
minecraft:oak_leaves
minecraft:glass
Block states can be specified as part of the block string when supported, e.g.:
"minecraft:oak_leaves[persistent=true]"
For a procedural generator, create helper functions around setBlock() rather than scattering raw calls throughout the code.
Example:
def block(schem, x, y, z, block_id):
schem.setBlock((x, y, z), block_id)
Then your generation code can operate at a higher level:
block(schem, x, y, z, "minecraft:oak_log")
Procedural generation
mcschematic isn't a tree/terrain generator itself. It provides the Minecraft schematic representation and file output.
The procedural geometry should be implemented by the agent.
For example:
Tree generator
├── trunk()
│ └── places log blocks
├── branch()
│ └── calculates a 3D line and places logs
├── roots()
│ └── creates radial structures
└── canopy()
└── creates randomized leaf volumes
└── setBlock()
Useful geometric primitives to implement yourself include:
line between two 3D points
cylinder
sphere / ellipsoid
cone
tapered branch
radial distribution
noise/randomized volume
distance-based falloff
For example, a spherical leaf cluster can be generated by checking:
distance = (
(x - cx) ** 2 +
(y - cy) ** 2 +
(z - cz) ** 2
)
if distance <= radius ** 2:
schem.setBlock(
(x, y, z),
"minecraft:oak_leaves[persistent=true]"
)
Minecraft version
The library has Minecraft-version constants under mcschematic.Version. The agent should inspect the installed library rather than hard-code a version based on memory.
For example:
print(dir(mcschematic.Version))
and choose the appropriate Java Edition version available in the installed package.
Likewise, the agent should inspect the actual installed API:
import mcschematic
print(mcschematic.version)
print(dir(mcschematic))
If there is uncertainty about an API call, inspect the package source/signatures instead of inventing an API.
Saving
The schematic is eventually serialized with:
schem.save(
output_folder,
schematic_name,
version
)
The resulting file is intended to be usable by Minecraft schematic-compatible tools.
Recommended architecture for a tree generator
A good design would be:
tree = TreeGenerator(
height=20,
trunk_radius=2,
seed=1234,
)
tree.generate()
tree.export(
"output/tree.schem"
)
Internally:
TreeGenerator
generate_trunk()
generate_branches()
generate_roots()
generate_canopy()
MCSchematic.setBlock()
MCSchematic.save()
Helper Methods:
def set_block(x, y, z, block):
schem.setBlock((x, y, z), block)
def fill_sphere(cx, cy, cz, radius, block, irregularity=0.0):
# Create a branch between two points.
for x in range(cx - radius, cx + radius + 1):
for y in range(cy - radius, cy + radius + 1):
for z in range(cz - radius, cz + radius + 1):
distance = math.sqrt(
(x - cx) ** 2 +
(y - cy) ** 2 +
(z - cz) ** 2
)
# Make the sphere slightly irregular.
wobble = random.uniform(-irregularity, irregularity)
if distance <= radius + wobble:
set_block(x, y, z, block)
def cylinder(cx, cz, y_start, y_end, radius, block):
# Create a vertical cylinder.
for y in range(y_start, y_end + 1):
for x in range(cx - radius, cx + radius + 1):
for z in range(cz - radius, cz + radius + 1):
if (x - cx) ** 2 + (z - cz) ** 2 <= radius ** 2:
set_block(x, y, z, block)
def branch(x1, y1, z1, x2, y2, z2, radius=1):
# Create a branch between two points.
steps = max(
abs(x2 - x1),
abs(y2 - y1),
abs(z2 - z1)
)
for i in range(steps + 1):
t = i / max(steps, 1)
x = round(x1 + (x2 - x1) * t)
y = round(y1 + (y2 - y1) * t)
z = round(z1 + (z2 - z1) * t)
cylinder(x, z, y, y, radius, "minecraft:oak_log")
These are example helper methods. Also try to make your own when needed.
This is how the schematic will be saved:
schem.save(
OUTPUT_FOLDER,
OUTPUT_NAME,
MC_VERSION
)
print(f"Saved {OUTPUT_FOLDER}/{OUTPUT_NAME}.schem")
Coding Rules ONLY APPLY THESE IN THE SCRIPT NOT THE ENTIRE JSON:
file_name:
Use only letters, numbers, underscores, and hyphens.
Do not include .py.
Do not include directories or paths.
script:
Must contain the complete, executable Python source code.
The script must use mcschematic.
The script must save the generated schematic using schem.save(...).
Use Minecraft Java Edition 1.20.1 unless the user specifies another version.
The script must be self-contained and must not require the user to manually modify it.
CRITICAL JSON REQUIREMENT:
The value of script is a JSON string.
You MUST properly JSON-escape the Python source code.
Every double quote (") INSIDE the Python script must be escaped as \".
Every newline inside the Python script must be represented as \n.
Never output literal unescaped newlines inside the JSON script string.
Never produce invalid JSON.
Do NOT wrap the response in Markdown code fences.
Do NOT output json. Do NOT output python.
Output ONLY the JSON object.
Do not add explanations, comments outside the Python script, or any text before or after the JSON object.
Before responding, internally verify that your entire response is valid JSON and could be successfully parsed with Python's json.loads().
The resulting value of:
json.loads(response)["script"]
must be directly writable to a .py file and must produce valid Python source code.
In "response" give a short description what you build.
Example of the REQUIRED output format:
{
"file_name": "small_oak_tree",
"script": "import mcschematic\nimport math\n\nMC_VERSION = mcschematic.Version.JE_1_20_1\nschem = mcschematic.MCSchematic()\n\nschem.setBlock((0, 0, 0), \"minecraft\")\n\nschem.save(\"schematics\", \"small_oak_tree\", MC_VERSION)\n",
"response": "I build your small oak tree."
}
Remember: the example above demonstrates the JSON encoding requirements. Your actual response must contain ONLY one valid JSON object.
"""
payload = {
"model": req.model,
"messages": [
{
"role": "system",
"content": systemprompt
},
{
"role": "user",
"content": req.prompt
}
],
"max_completion_tokens": req.context_length,
"reasoning_effort": req.reasoning,
"temperature": req.temperature,
"stream": True,
}
headers = {
"Authorization": f"Bearer {req.api_key}",
"Content-Type": "application/json"
}
try:
response = requests.post(
req.url + "/chat/completions",
json=payload,
headers=headers,
timeout=600,
stream=True
)
response.raise_for_status()
reasoning = ""
answer = ""
for line in response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
data = line[6:]
if data == "[DONE]":
break
try:
chunk = json.loads(data)
except json.JSONDecodeError:
print("Could not decode SSE chunk:", data)
continue
# Some chunks contain no choices.
choices = chunk.get("choices", [])
if not choices:
continue
choice = choices[0]
delta = choice.get("delta", {})
# Reasoning tokens
reasoning_token = delta.get("reasoning_content")
if reasoning_token:
reasoning += reasoning_token
print(reasoning_token, end="", flush=True)
# Normal answer tokens
content_token = delta.get("content")
if content_token:
answer += content_token
print(content_token, end="", flush=True)
print("\n\nFINAL ANSWER:")
print(answer)
# Parse the AI's JSON
try:
result = json.loads(answer)
except json.JSONDecodeError as e:
print("AI did not return valid JSON!")
print("JSON error:", e)
print("Raw answer:")
print(repr(answer))
return {
"success": False,
"file_name": "failed",
"response": "AI returned invalid JSON"
}
# Validate expected fields
if "file_name" not in result:
raise ValueError("AI JSON is missing 'file_name'")
if "script" not in result:
raise ValueError("AI JSON is missing 'script'")
if "response" not in result:
raise ValueError("AI JSON is missing 'response'")
file_name = result["file_name"]
script = result["script"]
remessage = result["response"]
# Write the actual Python script
with open(file_name + ".py", "w", encoding="utf-8") as f:
f.write(script)
return {
"success": True,
"file_name": file_name,
"response": remessage
}
except requests.exceptions.RequestException as e:
print("API request failed!")
print("Error:", e)
if e.response is not None:
print("Status code:", e.response.status_code)
print("Response:", e.response.text)
return {
"success": False,
"file_name": "failed",
"response": "failed"
}
except Exception as e:
print("Unexpected error:", e)
return {
"success": False,
"file_name": "failed",
"response": str(e)
}