import os
import json
import time
import threading
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from unsloth import FastLanguageModel
# 1. IMPORT YOUR NATIVE CLIENT (No more broken OpenEnv imports!)
from envs.free_guy.client import FreeGuyEnv
from envs.free_guy.models import FreeGuyAction, FreeGuyObservation, FreeGuyState
# ==========================================
# 2. CLOUD MODEL LOADING
# ==========================================
print("🧠 Downloading & Loading Trained LoRA Model from Hugging Face...")
hf_token = os.getenv("HF_TOKEN")
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "NikhilKhatri/FreeGuy-Combat-LoRA", # Your model!
max_seq_length = 512,
dtype = None,
load_in_4bit = True,
token = hf_token
)
FastLanguageModel.for_inference(model)
# ==========================================
# 3. WEB SERVER & LOGGING SETUP
# ==========================================
app = FastAPI()
agent_logs = ["System Booting...", "Unreal Engine booting in background...", "Waiting for connection..."]
def log_msg(msg):
global agent_logs
print(msg)
agent_logs.append(msg)
if len(agent_logs) > 25:
agent_logs.pop(0)
# ==========================================
# 4. THE BACKGROUND INFERENCE LOOP
# ==========================================
def background_ai_loop():
log_msg("⏳ Waiting for Unreal Engine to boot and open port 8556...")
# Wrap the entire connection in an infinite retry loop!
while True:
try:
# Use your native FreeGuyEnv!
with FreeGuyEnv(base_url="http://127.0.0.1:8556").sync() as env:
log_msg("✅ Connected to Unreal Engine OpenEnv Server!")
episode = 1
while True:
result = env.reset()
obs = getattr(result, 'observation', result)
ep_reward = 0.0
log_msg(f"🎮 --- STARTING EPISODE {episode} ---")
while True:
enemy_state = "Idle"
if getattr(obs, 'CurrentOpponentAction', 0.0) == 2.0: enemy_state = "Flanking"
elif getattr(obs, 'CurrentOpponentAction', 0.0) == 3.0: enemy_state = "Attacking"
dist = getattr(obs, 'DistanceFromEnemy', 0.0)
our_hp = getattr(obs, 'OurHealth', 0.0)
enemy_hp = getattr(obs, 'EnemyHealth', 0.0)
obs_text = f"Distance {dist:.2f}m, Your Health {our_hp}, Enemy Health {enemy_hp}, Enemy {enemy_state}"
prompt = f"[INST] State: {obs_text}. What is your action? [/INST] "
inputs = tokenizer([prompt], return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=40, use_cache=True)
llm_response = tokenizer.batch_decode(outputs[:, inputs.input_ids.shape[1]:], skip_special_tokens=True)[0]
action_dict = {"W": 0.0, "A": 0.0, "S": 0.0, "D": 0.0, "Attack": 0.0}
try:
start = llm_response.find('{')
end = llm_response.rfind('}') + 1
if start != -1 and end != -1:
clean_json_str = llm_response[start:end]
parsed_json = json.loads(clean_json_str)
for key in action_dict.keys():
raw_val = float(parsed_json.get(key, 0.0))
action_dict[key] = 1.0 if raw_val > 0.0 else 0.0
except Exception:
pass
action = FreeGuyAction(**action_dict)
for _ in range(30):
result = env.step(action)
obs = getattr(result, 'observation', result)
current_reward = getattr(obs, 'reward', 0.0)
ep_reward += current_reward
if getattr(result, 'done', False) or getattr(obs, 'terminated', False):
break
log_msg(f"Action Taken: {action_dict} | Reward: {current_reward:.2f}")
if getattr(result, 'done', False) or getattr(obs, 'terminated', False):
log_msg(f"💀 Episode {episode} Ended | Total Reward: {ep_reward:.2f}")
break
episode += 1
time.sleep(2)
except Exception as e:
log_msg(f"⏳ Engine not ready yet, retrying in 5 seconds... ({e})")
time.sleep(5) # Wait 5 seconds and try connecting again!
@app.on_event("startup")
def startup_event():
thread = threading.Thread(target=background_ai_loop)
thread.daemon = True
thread.start()
# ==========================================
# 5. THE WEB UI FOR THE JUDGES
# ==========================================
@app.get("/")
def read_root():
log_html = "
".join(agent_logs)
html_content = f"""
🤖 Live LLM Reinforcement Learning Agent
Environment: Unreal Engine 5 | Model: Qwen LoRA
This space is running a headless 3D physics engine and a live LLM simultaneously.