File size: 9,840 Bytes
0f33092 4793efe 0f33092 99e8434 4793efe 0f33092 4793efe 0f33092 99e8434 4793efe 99e8434 0f33092 4793efe cdd1cde 4793efe 99e8434 4793efe 99e8434 4793efe 99e8434 4793efe cdd1cde 4793efe cdd1cde 4793efe ba69a14 4793efe 0f33092 4793efe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
import gradio as gr
import torch
import requests
import json
import time
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
BASE_MODEL = "Qwen/Qwen2.5-0.5B"
PEFT_REPO = "coderofpears/gimkit-copilot"
SB_URL = "https://unztspukhwdzrllavnef.supabase.co"
SB_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVuenRzcHVraHdkenJsbGF2bmVmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM2MDMzNTAsImV4cCI6MjA4OTE3OTM1MH0.eUWx9O9uX-BqI7oUikVvSAwljgt4Gw3Fl6hb-Xz_YWo"
ALL_TOOLS = [
"gkc_create_build","gkc_undo_last_build","gkc_get_map_devices",
"gkc_look_around","gkc_capture_screenshot","gkc_remove_elements",
"gkc_vc_checkpoint","gkc_list_devices","gkc_search_terrain",
"gkc_search_props","gkc_get_device_info","gkc_skill_search",
"gkc_web_search","gkc_wiki_search",
]
# ββ Load model once βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
print("Loading model...")
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
_base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
device_map="auto" if torch.cuda.is_available() else None,
trust_remote_code=True,
)
model = PeftModel.from_pretrained(_base, PEFT_REPO)
model.eval()
DEVICE = next(model.parameters()).device
print("Model ready.")
# ββ Supabase helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def base_h():
return {"Content-Type": "application/json", "apikey": SB_ANON}
def auth_h(token):
return {**base_h(), "Authorization": "Bearer " + token}
def queue_tool(token, name, args, timeout=30):
r = requests.post(
SB_URL + "/rest/v1/tool_calls",
headers={**auth_h(token), "Prefer": "return=representation"},
json={"name": name, "args": args, "status": "pending"},
timeout=10,
)
r.raise_for_status()
row_id = r.json()[0]["id"]
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(1.2)
pr = requests.get(
SB_URL + f"/rest/v1/tool_calls?id=eq.{row_id}&select=status,result",
headers=auth_h(token), timeout=10,
)
pr.raise_for_status()
rows = pr.json()
if rows and rows[0]["status"] in ("complete", "error"):
return rows[0]["result"], rows[0]["status"]
return {"error": "Timed out β is the Gimloader plugin open and connected?"}, "error"
# ββ Model inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SYSTEM = (
"You are Gimkit Copilot, an AI that helps build Gimkit Creative maps. "
"Call tools by responding with JSON: {\"tool\":\"<name>\",\"args\":{...}} "
"Otherwise reply normally. Tools: " + ", ".join(ALL_TOOLS)
)
def run_model(history, user_msg):
prompt = SYSTEM + "\n\n"
for u, a in history:
prompt += f"User: {u}\nAssistant: {a}\n"
prompt += f"User: {user_msg}\nAssistant:"
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=1024).to(DEVICE)
with torch.no_grad():
out = model.generate(
**inputs,
max_new_tokens=256,
do_sample=True,
temperature=0.7,
top_p=0.9,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=tokenizer.eos_token_id,
)
new_tokens = out[0][inputs["input_ids"].shape[1]:]
return tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
def try_parse_tool(text):
start, end = text.find("{"), text.rfind("}") + 1
if start == -1 or end == 0:
return None
try:
obj = json.loads(text[start:end])
if "tool" in obj and obj["tool"] in ALL_TOOLS:
return obj["tool"], obj.get("args", {})
except Exception:
pass
return None
def respond(message, history, token):
if not token or not token.strip():
history.append((message, "β Enter your GimkitCopilot token in the sidebar first."))
return history, ""
model_out = run_model(history, message)
tool_call = try_parse_tool(model_out)
if tool_call:
name, args = tool_call
history.append((message, f"π§ Calling `{name}`..."))
result, status = queue_tool(token.strip(), name, args)
icon = "β
" if status == "complete" else "β"
result_str = json.dumps(result, indent=2) if isinstance(result, dict) else str(result)
followup = run_model(
history + [(message, model_out)],
f"Tool result for {name}: {result_str}. Summarise what happened."
)
history[-1] = (message, f"π§ `{name}`\n\n{icon} ```\n{result_str}\n```\n\n{followup}")
else:
history.append((message, model_out))
return history, ""
# ββ UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;600&display=swap');
:root {
--bg:#070b07; --surface:#0c120c; --border:#162016;
--green:#4CAF50; --green-dim:#1a3a1a; --glow:rgba(76,175,80,.12);
--text:#cce0cc; --muted:#4a664a; --accent:#a3e635;
}
body, .gradio-container { background:var(--bg)!important; font-family:'DM Sans',sans-serif; color:var(--text); }
#logo { font-family:'Space Mono',monospace; color:var(--green); font-size:20px; font-weight:700; padding:16px 0 2px; }
#sub { color:var(--muted); font-size:11px; font-family:'Space Mono',monospace; margin-bottom:20px; }
.token-box textarea {
background:#040804!important; border:1px solid var(--border)!important;
color:var(--accent)!important; font-family:'Space Mono',monospace!important; font-size:11px!important;
border-radius:8px!important;
}
.token-box textarea:focus { border-color:var(--green)!important; box-shadow:0 0 0 2px var(--glow)!important; }
#chatbot { background:var(--bg)!important; border-color:var(--border)!important; }
.send-btn button {
background:var(--green-dim)!important; border:1px solid var(--green)!important;
color:var(--green)!important; font-family:'Space Mono',monospace!important; font-weight:700!important;
}
.send-btn button:hover { background:#223a22!important; }
.clear-btn button {
background:transparent!important; border:1px solid var(--border)!important;
color:var(--muted)!important; font-size:11px!important;
}
.msg-box textarea {
background:var(--surface)!important; border:1px solid var(--border)!important;
color:var(--text)!important; font-family:'DM Sans',sans-serif!important; border-radius:8px!important;
}
.msg-box textarea:focus { border-color:var(--green)!important; }
"""
with gr.Blocks(title="Gimkit Copilot") as demo:
token_state = gr.State("")
with gr.Row():
# ββ Left sidebar: token input βββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=1, min_width=220):
gr.HTML('<div id="logo">β Gimkit<br>Copilot</div>')
gr.HTML('<div id="sub">Qwen2.5-0.5B + MCP</div>')
token_box = gr.Textbox(
label="GimkitCopilot Token",
placeholder="Paste Bearer tokenβ¦",
lines=4,
type="password",
elem_classes=["token-box"],
)
gr.HTML(
'''<div style="font-size:10px;color:#2a4a2a;line-height:1.6;margin-top:6px;">
Get token from the<br>Gimloader plugin:<br>
<b style="color:#3a6a3a">Shift+X β Account<br>β Copy Token</b><br><br>
Plugin must show<br><span style="color:#4CAF50">β Connected</span> for<br>map tools to work.
</div>'''
)
clear_btn = gr.Button("Clear chat", size="sm", elem_classes=["clear-btn"])
# ββ Right: chat βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=4):
chatbot = gr.Chatbot(
elem_id="chatbot",
height=520,
show_label=False,
placeholder="<div style=\'text-align:center;color:#2a4a2a;font-family:Space Mono,monospace;margin-top:40px\'>Sign in via token and start building β</div>",
)
with gr.Row():
msg_box = gr.Textbox(
placeholder="Ask me to build something, inspect your map, search the wikiβ¦",
show_label=False,
lines=2,
scale=8,
elem_classes=["msg-box"],
)
send_btn = gr.Button("Send βΆ", variant="primary", scale=1, elem_classes=["send-btn"])
# wire up token box β state
token_box.change(lambda t: t, inputs=[token_box], outputs=[token_state])
send_btn.click(respond, inputs=[msg_box, chatbot, token_state], outputs=[chatbot, msg_box])
msg_box.submit(respond, inputs=[msg_box, chatbot, token_state], outputs=[chatbot, msg_box])
clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
demo.launch(css=CSS)
|