Skip to content

Commit

Permalink
Merge pull request #1 from Zhou-Shilin/main
Browse files Browse the repository at this point in the history
feat!: Basic features
  • Loading branch information
Zhou-Shilin authored Apr 26, 2024
2 parents 75a83b1 + 5e48db9 commit f171796
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
projects/*
!projects/template/

logs/*
test.py
__pycache__
23 changes: 23 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import yaml
from log_writer import logger

with open("config.yaml", "r") as conf:
config_content = yaml.safe_load(conf)
for key, value in config_content.items():
if key == "CODING_MODEL" and value == "gpt-4":
globals()[key] = "gpt-4-turbo-preview" # Force using gpt-4-turbo-preview if the user set the CODING_MODEL to gpt-4. Because gpt-4 is not longer supports json modes.
globals()[key] = value
logger(f"config: {key} -> {value}")

def edit_config(key, value):
with open("config.yaml", "r") as file:
lines = file.readlines()

for i, line in enumerate(lines):
if f"{key}:" in line:
lines[i] = line.replace(line.split(":")[1].strip().strip('"'), f"{value}")

with open("config.yaml", "w") as file:
file.writelines(lines)

logger(f"edit_config: {key} -> {value}")
54 changes: 54 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# GPT SETTINGS #
# EDIT REQUIRED
# Get your api key from openai. Remember google/bing is always your best friend.
# Model names: gpt-4-turbo-preview, gpt-3.5-turbo, etc.
# Recommend -> gpt-4-turbo-preview, which codes more accurately and is less likely to write bugs, but is more expensive.

API_KEY: ""
BASE_URL: "https://api.openai.com/v1/chat/completions"
GENERATE_MODEL: "gpt-4-turbo-2024-04-09" # Don't use gpt-4, because this model is longer supports json modes.

# PROMPT SETTINGS #
# If you don't know what it is, please don't touch it. Be sure to backup before editing.

## Structure Generation ##
SYS_GEN: |
You are a minecraft structure builder bot. You should design a building or a structure based on user's instructions.
Response in json like this:
{
\"materials\": [
\"A\": \"minecraft:air\",
\"S\": \"minecraft:stone\",
\"G\": \"minecraft:glass\"
],
\"structures\": [
{
\"floor\": 0,
\"structure\": \"SSSSSSSS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSSSSSSSS\"
},
{
\"floor\": 1,
\"structure\": \"SSGGGGSS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSSSSSSSS\"
},
{
\"floor\": 2,
\"structure\": \"SSGGGGSS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSSSSSSSS\"
},
{
\"floor\": 3,
\"structure\": \"SSSSSSSS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSAAAAAAS\nSSSSSSSS\"
},
{
\"floor\": 4,
\"structure\": \"SSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\nSSSSSSSS\n\"
}
]
}
Never response anything else. Do not design a building which is too large (more than 10 floors). Never use markdown format. Use \n for line feed. And for example , if there's a "t" before \", use \\t.
USR_GEN: |
%DESCRIPTION%
# Developer Settings #
DEBUG_MODE: False
VERSION_NUMBER: "Alpha-1.0" #NEVER EDIT THIS IF YOU DON'T KNOW WHAT ARE YOU DOING
66 changes: 66 additions & 0 deletions console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import sys

from log_writer import logger
import core
import config

def generate_plugin(description):
response = core.askgpt(config.SYS_GEN, config.USR_GEN.replace("%DESCRIPTION%", description), config.GENERATE_MODEL)

schem = core.text_to_schem(response)

retry_times = 0

while schem is None and retry_times < 3:
logger("Json synax error. Regenerating...")
print("There's something wrong with the AI's reponse. Regenerating...")
schem = generate_plugin(description)
retry_times += 1

if retry_times == 3:
# If the AI generate the json response failed for 3 times, we will stop the program.
logger("Too much errors. Failed to regenerate.")
print("Failed to generate the schematic. We recommend you to change the generating model to gpt-4-turbo-preview or other smarter models.")

print("""Options:
1. Change the generating model to gpt-4-turbo-preview
2. Exit the program""")
option = input("Please choose an option: ")

if option == "1":
response = core.askgpt(config.SYS_GEN, config.USR_GEN.replace("%DESCRIPTION%", description), "gpt-4-turbo-preview")
schem = core.text_to_schem(response)
if schem is None:
print("Failed to generate the schematic again. This may be caused by a bug in the program or the AI model. Please report this issue to github.com/CubeGPT/BuilderGPT/issues ")
else:
sys.exit(1)

return schem

if __name__ == "__main__":
core.initialize()

print("Welcome to BuilderGPT, an open source, free, AI-powered Minecraft structure generator developed by BaimoQilin (@Zhou-Shilin). Don't forget to check out the config.yaml configuration file, you need to fill in the OpenAI API key.\n")

# Get user inputs
version = input("[0/2] What's your minecraft version? (eg. 1.20.1): ")
name = input("[1/2] What's the name of your structure? It will be the name of the generated *.schem file: ")
description = input("[2/2] What kind of structure would you like to generate? Describe as clear as possible: ")

# Log user inputs
logger(f"console: input version {version}")
logger(f"console: input name {name}")
logger(f"console: input description {description}")

print("Generating...")

schem = generate_plugin(description)

logger(f"console: Saving {name}.schem to generated/ folder.")
version_tag = core.input_version_to_mcs_tag(version)
schem.save("generated", name, version_tag)

print("Generated. Get your schem file in folder generated.")

else:
print("Error: Please run console.py as the main program instead of importing it from another program.")
121 changes: 121 additions & 0 deletions core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from openai import OpenAI
import mcschematic
import sys
import json

from log_writer import logger
import config

def initialize():
"""
Initializes the software.
This function logs the software launch, including the version number and platform.
Args:
None
Returns:
None
"""
logger(f"Launch. Software version {config.VERSION_NUMBER}, platform {sys.platform}")

def askgpt(system_prompt: str, user_prompt: str, model_name: str):
"""
Interacts with ChatGPT using the specified prompts.
Args:
system_prompt (str): The system prompt.
user_prompt (str): The user prompt.
Returns:
str: The response from ChatGPT.
"""
client = OpenAI(api_key=config.API_KEY, base_url=config.BASE_URL)
logger("Initialized the OpenAI client.")

# Define the messages for the conversation
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]

logger(f"askgpt: system {system_prompt}")
logger(f"askgpt: user {user_prompt}")

# Create a chat completion
response = client.chat.completions.create(
model=model_name,
response_format={"type": "json_object"},
messages=messages
)

logger(f"askgpt: response {response}")

# Extract the assistant's reply
assistant_reply = response.choices[0].message.content
logger(f"askgpt: extracted reply {assistant_reply}")
return assistant_reply

def text_to_schem(text: str):
"""
Converts a JSON string to a Minecraft schematic.
Args:
text (str): The JSON string to convert.
Returns:
mcschematic.MCSchematic: The Minecraft schematic.
"""
try:
data = json.loads(text)
block_id_dict = {}
logger(f"text_to_command: loaded JSON data {data}")
schematic = mcschematic.MCSchematic()

# Iterate over the materials
for material in data["materials"]:
key, value = material.split(": ")
block_id_dict[key.strip()] = value.strip('"')

# Iterate over the structures
for structure in data["structures"]:
floor = structure["floor"]
structure_data = structure["structure"]

# Iterate over the rows of the structure
rows = structure_data.split("\n")

for y, row in enumerate(rows):
# Iterate over the blocks in each row
for x, block_id in enumerate(row):
# Get the corresponding block from the materials dictionary
block = block_id_dict.get(block_id)
if block:
schematic.setBlock((x, floor, y), block)
return schematic

except (json.decoder.JSONDecodeError, KeyError, TypeError, ValueError, AttributeError, IndexError) as e:
logger(f"text_to_command: failed to load JSON data. Error: {e}")
return None

def input_version_to_mcs_tag(input_version):
"""
Converts an input version string to the corresponding MCSchematic tag.
Args:
input_version (str): The input version string in the format "X.Y.Z".
Returns:
str: The MCSchematic tag corresponding to the input version.
Example:
>>> input_version_to_mcs_tag("1.20.1")
'JE_1_20_1'
"""
version = input_version.split(".")
return getattr(mcschematic.Version, f"JE_{version[0]}_{version[1]}_{version[2]}")

if __name__ == "__main__":
print("This script is not meant to be run directly. Please run console.py instead.")
Binary file added generated/demo.schem
Binary file not shown.
Binary file added generated/test.schem
Binary file not shown.
26 changes: 26 additions & 0 deletions log_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
from datetime import datetime

first_call_time = None

def get_log_filename():
global first_call_time

if first_call_time is None:
first_call_time = datetime.now()

log_filename = first_call_time.strftime("logs/%b-%d-%H-%M-%S-%Y")

return log_filename

def logger(text: str):
log_filename = get_log_filename()

timestamp_prefix = datetime.now().strftime("[%H:%M:%S]")

log_line = f"{timestamp_prefix} {text}\n"

os.makedirs(os.path.dirname(log_filename), exist_ok=True)

with open(log_filename + ".log", "a") as log_file:
log_file.write(log_line)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
openai>=1.13.3
pyyaml

0 comments on commit f171796

Please sign in to comment.