feat: Initial Commit
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user