Spaces:
Sleeping
Sleeping
Commit Β·
5c40041
0
Parent(s):
Clean repository root for HF Spaces
Browse files- .gitignore +30 -0
- Dockerfile +68 -0
- inference.py +280 -0
- models.py +179 -0
- nginx.conf +57 -0
- openenv.yaml +84 -0
- requirements.txt +21 -0
- server/__init__.py +1 -0
- server/app.py +164 -0
- server/environment.py +172 -0
- start.sh +35 -0
.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ Python ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
|
| 12 |
+
# ββ Node / Vite βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
+
ui/node_modules/
|
| 14 |
+
ui/.vite/
|
| 15 |
+
ui/dist/
|
| 16 |
+
|
| 17 |
+
# ββ Environment secrets β NEVER commit these βββββββββββββββββββββββββββββββββ
|
| 18 |
+
.env
|
| 19 |
+
.env.local
|
| 20 |
+
.env.*.local
|
| 21 |
+
ui/.env
|
| 22 |
+
ui/.env.local
|
| 23 |
+
ui/.env.*.local
|
| 24 |
+
|
| 25 |
+
# ββ OS / Editor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
.vscode/
|
| 29 |
+
.idea/
|
| 30 |
+
*.swp
|
Dockerfile
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# Stage 1 β Build the Vite / React frontend
|
| 3 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
FROM node:20-slim AS frontend-builder
|
| 5 |
+
|
| 6 |
+
WORKDIR /app/ui
|
| 7 |
+
|
| 8 |
+
# Install deps first (better layer caching)
|
| 9 |
+
COPY ui/package.json ui/package-lock.json* ./
|
| 10 |
+
RUN npm ci --prefer-offline
|
| 11 |
+
|
| 12 |
+
# Copy all UI source
|
| 13 |
+
COPY ui/ ./
|
| 14 |
+
|
| 15 |
+
# Vite bakes VITE_* vars into the static bundle at build time.
|
| 16 |
+
# HF Spaces forwards the matching secrets as Docker build-args automatically
|
| 17 |
+
# when you set them in Space Settings β Variables and secrets.
|
| 18 |
+
ARG VITE_HF_TOKEN=""
|
| 19 |
+
ARG VITE_HF_MODEL="mistralai/Mistral-7B-Instruct-v0.2"
|
| 20 |
+
ENV VITE_HF_TOKEN=${VITE_HF_TOKEN}
|
| 21 |
+
ENV VITE_HF_MODEL=${VITE_HF_MODEL}
|
| 22 |
+
|
| 23 |
+
RUN npm run build
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
# Stage 2 β Python backend + Nginx to serve frontend
|
| 28 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
FROM python:3.11-slim
|
| 30 |
+
|
| 31 |
+
# System packages: nginx for static serving + reverse-proxy
|
| 32 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 33 |
+
nginx \
|
| 34 |
+
curl \
|
| 35 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 36 |
+
|
| 37 |
+
WORKDIR /app
|
| 38 |
+
|
| 39 |
+
# Python dependencies
|
| 40 |
+
COPY requirements.txt .
|
| 41 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 42 |
+
|
| 43 |
+
# Application source
|
| 44 |
+
COPY models.py .
|
| 45 |
+
COPY inference.py .
|
| 46 |
+
COPY server/ ./server/
|
| 47 |
+
COPY openenv.yaml .
|
| 48 |
+
|
| 49 |
+
# Nginx configuration
|
| 50 |
+
COPY nginx.conf /etc/nginx/nginx.conf
|
| 51 |
+
|
| 52 |
+
# Built frontend from stage 1
|
| 53 |
+
COPY --from=frontend-builder /app/ui/dist ./ui/dist
|
| 54 |
+
|
| 55 |
+
# Startup script
|
| 56 |
+
COPY start.sh /start.sh
|
| 57 |
+
RUN chmod +x /start.sh
|
| 58 |
+
|
| 59 |
+
# HF Spaces exposes port 7860
|
| 60 |
+
EXPOSE 7860
|
| 61 |
+
|
| 62 |
+
# Runtime environment (overridden by HF Space secrets at runtime)
|
| 63 |
+
ENV PORT=7860 \
|
| 64 |
+
HF_MODEL="mistralai/Mistral-7B-Instruct-v0.2" \
|
| 65 |
+
HF_TOKEN="" \
|
| 66 |
+
PYTHONUNBUFFERED=1
|
| 67 |
+
|
| 68 |
+
CMD ["/start.sh"]
|
inference.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
OpenEnv-compliant inference environment for Survival Island.
|
| 5 |
+
Provides step(), reset(), state() methods and task graders.
|
| 6 |
+
|
| 7 |
+
Uses OpenAI client format with [START], [STEP], [END] logging.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
import os
|
| 13 |
+
from typing import Any, Dict, Optional, Tuple
|
| 14 |
+
|
| 15 |
+
from openai import OpenAI
|
| 16 |
+
|
| 17 |
+
# ββ Logging setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ββ OpenEnv Environment βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
class SurvivalIslandEnvironment:
|
| 25 |
+
"""
|
| 26 |
+
OpenEnv-compliant environment wrapper for Survival Island.
|
| 27 |
+
Provides the required step(), reset(), state() interface.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self):
|
| 31 |
+
"""Initialize the environment."""
|
| 32 |
+
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", ""))
|
| 33 |
+
self.generation = 0
|
| 34 |
+
self.total_resources_collected = 0
|
| 35 |
+
self.challenges_won = 0
|
| 36 |
+
self.current_state = self._create_initial_state()
|
| 37 |
+
|
| 38 |
+
logger.info("[START] Survival Island environment initialized")
|
| 39 |
+
|
| 40 |
+
def _create_initial_state(self) -> Dict[str, Any]:
|
| 41 |
+
"""Create initial game state."""
|
| 42 |
+
return {
|
| 43 |
+
"generation": 0,
|
| 44 |
+
"health": 100.0,
|
| 45 |
+
"hunger": 50.0,
|
| 46 |
+
"thirst": 50.0,
|
| 47 |
+
"stamina": 100.0,
|
| 48 |
+
"fear": 0.0,
|
| 49 |
+
"wood": 0,
|
| 50 |
+
"stone": 0,
|
| 51 |
+
"food": 0,
|
| 52 |
+
"water": 0,
|
| 53 |
+
"playerX": 1000.0,
|
| 54 |
+
"isNight": False,
|
| 55 |
+
"inventory": {
|
| 56 |
+
"spear": False,
|
| 57 |
+
"bow": False,
|
| 58 |
+
"fishingRod": False,
|
| 59 |
+
"boat": False,
|
| 60 |
+
},
|
| 61 |
+
"baseCamp": {
|
| 62 |
+
"x": None,
|
| 63 |
+
"y": None,
|
| 64 |
+
"level": 0,
|
| 65 |
+
},
|
| 66 |
+
"memory": {
|
| 67 |
+
"evolutionLevel": 1,
|
| 68 |
+
"pastDeaths": [],
|
| 69 |
+
"totalGenerations": 0,
|
| 70 |
+
"challengesWon": 0,
|
| 71 |
+
},
|
| 72 |
+
"activeChallenge": None,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def reset(self) -> Dict[str, Any]:
|
| 76 |
+
"""Reset the environment to initial state."""
|
| 77 |
+
logger.info("[STEP] reset() called")
|
| 78 |
+
self.generation = 0
|
| 79 |
+
self.total_resources_collected = 0
|
| 80 |
+
self.challenges_won = 0
|
| 81 |
+
self.current_state = self._create_initial_state()
|
| 82 |
+
logger.info("[END] Environment reset complete")
|
| 83 |
+
return self.current_state
|
| 84 |
+
|
| 85 |
+
def step(self, action: str) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
|
| 86 |
+
"""
|
| 87 |
+
Execute one step in the environment.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
action: Action taken by the agent (e.g., "FORAGE", "HUNT", "FISH")
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
Tuple of (state, reward, done, info)
|
| 94 |
+
"""
|
| 95 |
+
logger.info(f"[STEP] Action: {action}")
|
| 96 |
+
|
| 97 |
+
# Simulate action effects
|
| 98 |
+
reward = 0.0
|
| 99 |
+
done = False
|
| 100 |
+
info = {}
|
| 101 |
+
|
| 102 |
+
action_upper = action.upper().strip()
|
| 103 |
+
|
| 104 |
+
# Resource gathering actions
|
| 105 |
+
if action_upper == "FORAGE":
|
| 106 |
+
self.current_state["food"] += 5
|
| 107 |
+
self.total_resources_collected += 5
|
| 108 |
+
reward = 0.1
|
| 109 |
+
elif action_upper == "FISH":
|
| 110 |
+
self.current_state["food"] += 8
|
| 111 |
+
self.total_resources_collected += 8
|
| 112 |
+
reward = 0.15
|
| 113 |
+
elif action_upper == "GET_WATER":
|
| 114 |
+
self.current_state["water"] += 10
|
| 115 |
+
self.total_resources_collected += 10
|
| 116 |
+
reward = 0.15
|
| 117 |
+
elif action_upper == "HUNT":
|
| 118 |
+
self.current_state["food"] += 20
|
| 119 |
+
self.total_resources_collected += 20
|
| 120 |
+
reward = 0.25
|
| 121 |
+
elif action_upper in ["CRAFT_SPEAR", "CRAFT_BOW", "CRAFT_ROD", "CRAFT_BOAT"]:
|
| 122 |
+
# Crafting actions grant survival tools
|
| 123 |
+
tool_key = action_upper.lower().replace("craft_", "").replace("_", "")
|
| 124 |
+
if tool_key in self.current_state["inventory"]:
|
| 125 |
+
self.current_state["inventory"][tool_key] = True
|
| 126 |
+
reward = 0.2
|
| 127 |
+
elif action_upper == "BUILD_CAMP":
|
| 128 |
+
if self.current_state["baseCamp"]["level"] == 0:
|
| 129 |
+
self.current_state["baseCamp"]["x"] = self.current_state["playerX"]
|
| 130 |
+
self.current_state["baseCamp"]["level"] = 1
|
| 131 |
+
reward = 0.3
|
| 132 |
+
elif action_upper == "UPGRADE_CAMP":
|
| 133 |
+
self.current_state["baseCamp"]["level"] = min(
|
| 134 |
+
self.current_state["baseCamp"]["level"] + 1, 3
|
| 135 |
+
)
|
| 136 |
+
reward = 0.25
|
| 137 |
+
elif action_upper == "FLEE":
|
| 138 |
+
reward = 0.1
|
| 139 |
+
elif action_upper == "WANDER":
|
| 140 |
+
reward = -0.05
|
| 141 |
+
|
| 142 |
+
# Decay resources each step
|
| 143 |
+
self.current_state["hunger"] = max(0, self.current_state["hunger"] - 2)
|
| 144 |
+
self.current_state["thirst"] = max(0, self.current_state["thirst"] - 1.5)
|
| 145 |
+
self.current_state["stamina"] = max(0, self.current_state["stamina"] - 1)
|
| 146 |
+
|
| 147 |
+
# Consume resources if available
|
| 148 |
+
if self.current_state["food"] > 0:
|
| 149 |
+
self.current_state["hunger"] = min(100, self.current_state["hunger"] + 3)
|
| 150 |
+
self.current_state["food"] -= 1
|
| 151 |
+
|
| 152 |
+
if self.current_state["water"] > 0:
|
| 153 |
+
self.current_state["thirst"] = min(100, self.current_state["thirst"] + 2)
|
| 154 |
+
self.current_state["water"] -= 1
|
| 155 |
+
|
| 156 |
+
# Check death conditions
|
| 157 |
+
if self.current_state["health"] <= 0:
|
| 158 |
+
done = True
|
| 159 |
+
info["death_reason"] = "health_depleted"
|
| 160 |
+
reward = -1.0
|
| 161 |
+
elif self.current_state["hunger"] <= 0:
|
| 162 |
+
self.current_state["health"] -= 10
|
| 163 |
+
reward -= 0.2
|
| 164 |
+
elif self.current_state["thirst"] <= 0:
|
| 165 |
+
self.current_state["health"] -= 15
|
| 166 |
+
reward -= 0.3
|
| 167 |
+
|
| 168 |
+
self.generation += 1
|
| 169 |
+
self.current_state["generation"] = self.generation
|
| 170 |
+
self.current_state["memory"]["totalGenerations"] = self.generation
|
| 171 |
+
|
| 172 |
+
logger.info(f"[END] Step {self.generation}: reward={reward:.3f}, done={done}")
|
| 173 |
+
return self.current_state, reward, done, info
|
| 174 |
+
|
| 175 |
+
def state(self) -> Dict[str, Any]:
|
| 176 |
+
"""Get current environment state."""
|
| 177 |
+
logger.info("[STEP] state() called")
|
| 178 |
+
logger.info("[END] State retrieved")
|
| 179 |
+
return self.current_state
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# ββ Task Graders (0.0 to 1.0) ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
|
| 184 |
+
class TaskGraders:
|
| 185 |
+
"""Graders for the 3 required tasks."""
|
| 186 |
+
|
| 187 |
+
@staticmethod
|
| 188 |
+
def grade_survival_expert(state: Dict[str, Any]) -> float:
|
| 189 |
+
"""
|
| 190 |
+
Task 1 (Easy): Survive for 50+ generations.
|
| 191 |
+
Grader: Returns (min(generations, 50) / 50)
|
| 192 |
+
"""
|
| 193 |
+
generations = state.get("generation", 0)
|
| 194 |
+
score = min(generations, 50) / 50.0
|
| 195 |
+
return min(score, 1.0)
|
| 196 |
+
|
| 197 |
+
@staticmethod
|
| 198 |
+
def grade_resourceful_gatherer(state: Dict[str, Any]) -> float:
|
| 199 |
+
"""
|
| 200 |
+
Task 2 (Medium): Collect 500+ total resources.
|
| 201 |
+
Grader: Returns (min(total_resources, 500) / 500)
|
| 202 |
+
"""
|
| 203 |
+
# Simulating total resources by checking inventory state
|
| 204 |
+
memory = state.get("memory", {})
|
| 205 |
+
total_resources = memory.get("challengesWon", 0) * 100 # Proxy metric
|
| 206 |
+
# Add actual collected resources
|
| 207 |
+
total_resources += (
|
| 208 |
+
state.get("wood", 0)
|
| 209 |
+
+ state.get("stone", 0)
|
| 210 |
+
+ (state.get("food", 0) * 2)
|
| 211 |
+
+ (state.get("water", 0) * 2)
|
| 212 |
+
)
|
| 213 |
+
score = min(total_resources, 500) / 500.0
|
| 214 |
+
return min(score, 1.0)
|
| 215 |
+
|
| 216 |
+
@staticmethod
|
| 217 |
+
def grade_challenge_master(state: Dict[str, Any]) -> float:
|
| 218 |
+
"""
|
| 219 |
+
Task 3 (Hard): Win 10+ challenges and reach evolution level 5.
|
| 220 |
+
Grader: Returns (min(challenges_won, 10) / 10) * (evolution_level / 5)
|
| 221 |
+
"""
|
| 222 |
+
memory = state.get("memory", {})
|
| 223 |
+
challenges_won = memory.get("challengesWon", 0)
|
| 224 |
+
evolution_level = memory.get("evolutionLevel", 1)
|
| 225 |
+
|
| 226 |
+
challenge_score = min(challenges_won, 10) / 10.0
|
| 227 |
+
evolution_score = min(evolution_level, 5) / 5.0
|
| 228 |
+
score = (challenge_score + evolution_score) / 2.0
|
| 229 |
+
return min(score, 1.0)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# ββ Main Entry Point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 233 |
+
|
| 234 |
+
def main():
|
| 235 |
+
"""Demonstrate environment usage."""
|
| 236 |
+
logger.info("[START] Initialization")
|
| 237 |
+
|
| 238 |
+
env = SurvivalIslandEnvironment()
|
| 239 |
+
graders = TaskGraders()
|
| 240 |
+
|
| 241 |
+
# Reset environment
|
| 242 |
+
state = env.reset()
|
| 243 |
+
logger.info(f"Initial state: generation={state['generation']}")
|
| 244 |
+
|
| 245 |
+
# Simulate a few steps
|
| 246 |
+
logger.info("[START] Simulation")
|
| 247 |
+
actions = ["FORAGE", "BUILD_CAMP", "CRAFT_SPEAR", "HUNT", "GET_WATER"]
|
| 248 |
+
|
| 249 |
+
for action in actions:
|
| 250 |
+
logger.info(f"[STEP] Executing action: {action}")
|
| 251 |
+
next_state, reward, done, info = env.step(action)
|
| 252 |
+
logger.info(f"[END] Step completed: reward={reward}, done={done}")
|
| 253 |
+
|
| 254 |
+
if done:
|
| 255 |
+
logger.info(f"[END] Episode terminated: {info}")
|
| 256 |
+
break
|
| 257 |
+
|
| 258 |
+
# Grade tasks
|
| 259 |
+
final_state = env.state()
|
| 260 |
+
logger.info("[START] Task Grading")
|
| 261 |
+
|
| 262 |
+
task1_score = graders.grade_survival_expert(final_state)
|
| 263 |
+
task2_score = graders.grade_resourceful_gatherer(final_state)
|
| 264 |
+
task3_score = graders.grade_challenge_master(final_state)
|
| 265 |
+
|
| 266 |
+
logger.info(f"[STEP] Task 1 (Survival Expert): {task1_score:.3f}")
|
| 267 |
+
logger.info(f"[STEP] Task 2 (Resourceful Gatherer): {task2_score:.3f}")
|
| 268 |
+
logger.info(f"[STEP] Task 3 (Challenge Master): {task3_score:.3f}")
|
| 269 |
+
|
| 270 |
+
logger.info("[END] All tasks graded")
|
| 271 |
+
|
| 272 |
+
print("\n--- Results ---")
|
| 273 |
+
print(f"Task 1 Score: {task1_score:.3f}")
|
| 274 |
+
print(f"Task 2 Score: {task2_score:.3f}")
|
| 275 |
+
print(f"Task 3 Score: {task3_score:.3f}")
|
| 276 |
+
print(f"Average Score: {(task1_score + task2_score + task3_score) / 3:.3f}")
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
models.py
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
Handles LLM inference for the Survival Island AI agent.
|
| 5 |
+
|
| 6 |
+
Priority chain
|
| 7 |
+
--------------
|
| 8 |
+
1. Local transformers pipeline (available on GPU Spaces β uncomment deps)
|
| 9 |
+
2. HuggingFace Inference API (works on CPU Spaces, needs HF_TOKEN)
|
| 10 |
+
3. Rule-based fallback (no network / no token)
|
| 11 |
+
|
| 12 |
+
Environment variables
|
| 13 |
+
---------------------
|
| 14 |
+
HF_TOKEN β HuggingFace API token (required for path 2)
|
| 15 |
+
HF_MODEL β Model repo ID, e.g. "mistralai/Mistral-7B-Instruct-v0.2"
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import json
|
| 21 |
+
import logging
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
import threading
|
| 25 |
+
from typing import Optional
|
| 26 |
+
|
| 27 |
+
import requests
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
HF_TOKEN: str = os.getenv("HF_TOKEN", "")
|
| 33 |
+
HF_MODEL: str = os.getenv("HF_MODEL", "mistralai/Mistral-7B-Instruct-v0.2")
|
| 34 |
+
|
| 35 |
+
VALID_ACTIONS: set[str] = {
|
| 36 |
+
"FORAGE", "HUNT", "FISH", "GET_WATER", "SEEK_SHELTER",
|
| 37 |
+
"BUILD_CAMP", "UPGRADE_CAMP", "CRAFT_SPEAR", "CRAFT_BOW",
|
| 38 |
+
"CRAFT_ROD", "CRAFT_BOAT", "EVACUATE", "FIGHT", "FLEE", "WANDER",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# ββ Singleton βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
_lock = threading.Lock()
|
| 43 |
+
_pipeline = None # transformers Pipeline object (optional)
|
| 44 |
+
_use_local: bool = False # True once local pipeline loaded successfully
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ββ Local pipeline (optional β GPU Spaces) ββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
def _try_load_local() -> bool:
|
| 49 |
+
"""
|
| 50 |
+
Attempts to load the model locally with transformers + torch.
|
| 51 |
+
Returns True on success. Skipped silently on CPU-only environments.
|
| 52 |
+
Uncomment torch/transformers in requirements.txt to enable this path.
|
| 53 |
+
"""
|
| 54 |
+
global _pipeline, _use_local
|
| 55 |
+
try:
|
| 56 |
+
import torch
|
| 57 |
+
from transformers import pipeline as hf_pipeline # type: ignore
|
| 58 |
+
|
| 59 |
+
logger.info(f"[models] Loading {HF_MODEL} locally β¦")
|
| 60 |
+
device = 0 if torch.cuda.is_available() else -1
|
| 61 |
+
_pipeline = hf_pipeline(
|
| 62 |
+
"text-generation",
|
| 63 |
+
model=HF_MODEL,
|
| 64 |
+
token=HF_TOKEN or None,
|
| 65 |
+
device=device,
|
| 66 |
+
torch_dtype=torch.float16 if device >= 0 else torch.float32,
|
| 67 |
+
max_new_tokens=80,
|
| 68 |
+
)
|
| 69 |
+
_use_local = True
|
| 70 |
+
logger.info(f"[models] Local pipeline ready (device={device})")
|
| 71 |
+
return True
|
| 72 |
+
except Exception as exc:
|
| 73 |
+
logger.warning(f"[models] Local pipeline skipped: {exc}")
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_pipeline():
|
| 78 |
+
"""Return the pipeline, initialising on first call."""
|
| 79 |
+
with _lock:
|
| 80 |
+
if _pipeline is None:
|
| 81 |
+
_try_load_local()
|
| 82 |
+
return _pipeline
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ββ Output parser βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
def _parse_action(raw: str) -> dict:
|
| 87 |
+
"""
|
| 88 |
+
Extract {"action": ..., "thought": ...} from model output.
|
| 89 |
+
Handles markdown fences, leading prose, trailing noise.
|
| 90 |
+
"""
|
| 91 |
+
# Strip markdown fences
|
| 92 |
+
cleaned = re.sub(r"```(?:json)?|```", "", raw, flags=re.IGNORECASE)
|
| 93 |
+
# Keep only the first JSON object
|
| 94 |
+
match = re.search(r"\{[^}]+\}", cleaned, re.DOTALL)
|
| 95 |
+
if not match:
|
| 96 |
+
raise ValueError(f"No JSON object in model output: {raw!r}")
|
| 97 |
+
obj = json.loads(match.group())
|
| 98 |
+
action = str(obj.get("action", "WANDER")).upper()
|
| 99 |
+
if action not in VALID_ACTIONS:
|
| 100 |
+
logger.warning(f"[models] Unknown action '{action}', defaulting to WANDER")
|
| 101 |
+
action = "WANDER"
|
| 102 |
+
return {
|
| 103 |
+
"action": action,
|
| 104 |
+
"thought": str(obj.get("thought", "Processingβ¦")),
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ββ Inference paths βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
def infer_local(prompt: str) -> dict:
|
| 110 |
+
"""Run generation using the locally loaded transformers pipeline."""
|
| 111 |
+
pipe = get_pipeline()
|
| 112 |
+
if pipe is None:
|
| 113 |
+
raise RuntimeError("Local pipeline not available")
|
| 114 |
+
outputs = pipe(
|
| 115 |
+
prompt,
|
| 116 |
+
max_new_tokens=80,
|
| 117 |
+
temperature=0.7,
|
| 118 |
+
do_sample=True,
|
| 119 |
+
return_full_text=False,
|
| 120 |
+
)
|
| 121 |
+
return _parse_action(outputs[0]["generated_text"])
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def infer_api(prompt: str) -> dict:
|
| 125 |
+
"""Call the HuggingFace Inference API (remote, no GPU needed)."""
|
| 126 |
+
if not HF_TOKEN:
|
| 127 |
+
raise RuntimeError("HF_TOKEN not set β cannot call Inference API")
|
| 128 |
+
url = f"https://api-inference.huggingface.co/models/{HF_MODEL}"
|
| 129 |
+
resp = requests.post(
|
| 130 |
+
url,
|
| 131 |
+
headers={
|
| 132 |
+
"Authorization": f"Bearer {HF_TOKEN}",
|
| 133 |
+
"Content-Type": "application/json",
|
| 134 |
+
},
|
| 135 |
+
json={
|
| 136 |
+
"inputs": prompt,
|
| 137 |
+
"parameters": {
|
| 138 |
+
"max_new_tokens": 80,
|
| 139 |
+
"temperature": 0.7,
|
| 140 |
+
"return_full_text": False,
|
| 141 |
+
"stop": ["\n\n", "</s>", "[INST]"],
|
| 142 |
+
},
|
| 143 |
+
},
|
| 144 |
+
timeout=30,
|
| 145 |
+
)
|
| 146 |
+
resp.raise_for_status()
|
| 147 |
+
data = resp.json()
|
| 148 |
+
raw = (
|
| 149 |
+
data[0]["generated_text"]
|
| 150 |
+
if isinstance(data, list)
|
| 151 |
+
else data.get("generated_text", "")
|
| 152 |
+
)
|
| 153 |
+
return _parse_action(raw)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def run_inference(prompt: str) -> dict:
|
| 157 |
+
"""
|
| 158 |
+
Main entry point used by server/app.py.
|
| 159 |
+
|
| 160 |
+
Tries local β API β rule-based fallback in that order.
|
| 161 |
+
Never raises β always returns a valid {"action", "thought"} dict.
|
| 162 |
+
"""
|
| 163 |
+
# 1. Local transformers pipeline (GPU Space)
|
| 164 |
+
if _use_local:
|
| 165 |
+
try:
|
| 166 |
+
return infer_local(prompt)
|
| 167 |
+
except Exception as exc:
|
| 168 |
+
logger.warning(f"[models] Local inference failed: {exc}")
|
| 169 |
+
|
| 170 |
+
# 2. HuggingFace Inference API (CPU Space)
|
| 171 |
+
if HF_TOKEN:
|
| 172 |
+
try:
|
| 173 |
+
return infer_api(prompt)
|
| 174 |
+
except Exception as exc:
|
| 175 |
+
logger.warning(f"[models] Inference API failed: {exc}")
|
| 176 |
+
|
| 177 |
+
# 3. Hard fallback
|
| 178 |
+
logger.error("[models] All inference paths failed β returning WANDER")
|
| 179 |
+
return {"action": "WANDER", "thought": "Inference offline. Wandering."}
|
nginx.conf
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
worker_processes 1;
|
| 2 |
+
error_log /dev/stderr warn;
|
| 3 |
+
pid /tmp/nginx.pid;
|
| 4 |
+
|
| 5 |
+
events {
|
| 6 |
+
worker_connections 1024;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
http {
|
| 10 |
+
include /etc/nginx/mime.types;
|
| 11 |
+
default_type application/octet-stream;
|
| 12 |
+
access_log /dev/stdout;
|
| 13 |
+
sendfile on;
|
| 14 |
+
tcp_nopush on;
|
| 15 |
+
keepalive_timeout 65;
|
| 16 |
+
|
| 17 |
+
gzip on;
|
| 18 |
+
gzip_types
|
| 19 |
+
text/plain
|
| 20 |
+
text/css
|
| 21 |
+
text/javascript
|
| 22 |
+
application/javascript
|
| 23 |
+
application/json
|
| 24 |
+
application/wasm
|
| 25 |
+
image/svg+xml;
|
| 26 |
+
|
| 27 |
+
server {
|
| 28 |
+
listen 7860;
|
| 29 |
+
server_name _;
|
| 30 |
+
|
| 31 |
+
root /app/ui/dist;
|
| 32 |
+
index index.html;
|
| 33 |
+
|
| 34 |
+
# ββ React SPA: always return index.html for unknown paths ββββββββββ
|
| 35 |
+
location / {
|
| 36 |
+
try_files $uri $uri/ /index.html;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# ββ Proxy /api/* β FastAPI on port 8000 βββββββββββββββββββββββββββ
|
| 40 |
+
location /api/ {
|
| 41 |
+
proxy_pass http://127.0.0.1:8000;
|
| 42 |
+
proxy_http_version 1.1;
|
| 43 |
+
proxy_set_header Host $host;
|
| 44 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 45 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 46 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 47 |
+
proxy_read_timeout 60s;
|
| 48 |
+
proxy_send_timeout 60s;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# ββ Static asset caching βββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
location ~* \.(js|css|wasm|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
| 53 |
+
expires 1y;
|
| 54 |
+
add_header Cache-Control "public, immutable";
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
openenv.yaml
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Survival_Island
|
| 2 |
+
version: "1.0.0"
|
| 3 |
+
description: >
|
| 4 |
+
An AI-powered 2-D survival simulation where a HuggingFace LLM agent
|
| 5 |
+
autonomously makes survival decisions across a procedurally-generated
|
| 6 |
+
6000 Γ 3000 world. Built for the Meta PyTorch Hackathon.
|
| 7 |
+
|
| 8 |
+
# ββ Hugging Face Space ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
space:
|
| 10 |
+
repo_id: suraj291/Survival_Island # change to your HF username/repo
|
| 11 |
+
sdk: docker
|
| 12 |
+
hardware: cpu-basic # upgrade to gpu-t4-small for local inference
|
| 13 |
+
title: "Survival Island β LLM Survival Agent"
|
| 14 |
+
emoji: ποΈ
|
| 15 |
+
colorFrom: green
|
| 16 |
+
colorTo: blue
|
| 17 |
+
pinned: false
|
| 18 |
+
|
| 19 |
+
# ββ Secrets / Variables (set in HF Space β Settings β Variables and secrets)
|
| 20 |
+
env:
|
| 21 |
+
secrets:
|
| 22 |
+
- name: HF_TOKEN
|
| 23 |
+
description: >
|
| 24 |
+
HuggingFace API token (read scope is enough for Inference API).
|
| 25 |
+
Also baked into the Vite bundle as VITE_HF_TOKEN at build time.
|
| 26 |
+
|
| 27 |
+
variables:
|
| 28 |
+
- name: HF_MODEL
|
| 29 |
+
default: "mistralai/Mistral-7B-Instruct-v0.2"
|
| 30 |
+
description: >
|
| 31 |
+
Any HF chat/instruct model compatible with the [INST] prompt format.
|
| 32 |
+
Alternatives: HuggingFaceH4/zephyr-7b-beta
|
| 33 |
+
meta-llama/Llama-3.1-8B-Instruct (needs license accept)
|
| 34 |
+
- name: PORT
|
| 35 |
+
default: "7860"
|
| 36 |
+
description: Port exposed by the Docker container (HF Spaces default).
|
| 37 |
+
|
| 38 |
+
# ββ Build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
build:
|
| 40 |
+
dockerfile: Dockerfile
|
| 41 |
+
context: .
|
| 42 |
+
steps:
|
| 43 |
+
- name: install_python_deps
|
| 44 |
+
run: pip install -r requirements.txt --no-cache-dir
|
| 45 |
+
|
| 46 |
+
- name: build_frontend
|
| 47 |
+
run: |
|
| 48 |
+
cd ui
|
| 49 |
+
npm ci
|
| 50 |
+
npm run build
|
| 51 |
+
env:
|
| 52 |
+
VITE_HF_TOKEN: "${HF_TOKEN}"
|
| 53 |
+
VITE_HF_MODEL: "${HF_MODEL}"
|
| 54 |
+
|
| 55 |
+
# ββ Runtime services ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
services:
|
| 57 |
+
backend:
|
| 58 |
+
type: fastapi
|
| 59 |
+
entrypoint: "uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 1"
|
| 60 |
+
healthcheck: "/api/health"
|
| 61 |
+
|
| 62 |
+
frontend:
|
| 63 |
+
type: static
|
| 64 |
+
serve_dir: "ui/dist"
|
| 65 |
+
|
| 66 |
+
# ββ Model / hardware ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
model:
|
| 68 |
+
framework: pytorch
|
| 69 |
+
task: text-generation
|
| 70 |
+
device: cpu # set to cuda on gpu-* hardware tiers
|
| 71 |
+
load_in_8bit: false
|
| 72 |
+
|
| 73 |
+
# ββ Push ignore list ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 74 |
+
push:
|
| 75 |
+
ignore:
|
| 76 |
+
- "ui/node_modules"
|
| 77 |
+
- "ui/.vite"
|
| 78 |
+
- "ui/dist"
|
| 79 |
+
- "__pycache__"
|
| 80 |
+
- "*.pyc"
|
| 81 |
+
- ".env"
|
| 82 |
+
- ".env.local"
|
| 83 |
+
- "ui/.env.local"
|
| 84 |
+
- ".git"
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ Web framework βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
fastapi==0.111.0
|
| 3 |
+
uvicorn[standard]==0.29.0
|
| 4 |
+
pydantic==2.7.1
|
| 5 |
+
|
| 6 |
+
# ββ HTTP (Inference API fallback) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
requests==2.32.3
|
| 8 |
+
httpx==0.27.0
|
| 9 |
+
|
| 10 |
+
# ββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
python-dotenv==1.0.1
|
| 12 |
+
|
| 13 |
+
# ββ OpenEnv inference ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
openai==1.3.0
|
| 15 |
+
|
| 16 |
+
# ββ Optional: local model inference ββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
# Uncomment these if you deploy on a GPU Space (hardware: gpu-t4-small or above)
|
| 18 |
+
# torch==2.3.0
|
| 19 |
+
# transformers==4.41.0
|
| 20 |
+
# accelerate==0.30.0
|
| 21 |
+
# bitsandbytes==0.43.1 # 8-bit quantisation β halves VRAM usage
|
server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# server package β FastAPI backend for Survival Island
|
server/app.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
server/app.py
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
FastAPI backend for Survival Island.
|
| 5 |
+
|
| 6 |
+
Endpoints
|
| 7 |
+
---------
|
| 8 |
+
GET /api/health liveness probe (used by start.sh and HF healthcheck)
|
| 9 |
+
GET /api/config safe public config (model name, pipeline status)
|
| 10 |
+
POST /api/infer LLM survival-action inference
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import time
|
| 18 |
+
from contextlib import asynccontextmanager
|
| 19 |
+
|
| 20 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 21 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
+
from fastapi.responses import JSONResponse
|
| 23 |
+
|
| 24 |
+
from server.environment import GameState, InferResponse, build_prompt
|
| 25 |
+
import models
|
| 26 |
+
|
| 27 |
+
# ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
logging.basicConfig(
|
| 29 |
+
level=logging.INFO,
|
| 30 |
+
format="%(asctime)s [%(levelname)-8s] %(name)s: %(message)s",
|
| 31 |
+
)
|
| 32 |
+
logger = logging.getLogger("survival.api")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ββ Lifespan (startup / shutdown) βββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
@asynccontextmanager
|
| 37 |
+
async def lifespan(app: FastAPI):
|
| 38 |
+
"""Pre-warm the model pipeline in a daemon thread on startup."""
|
| 39 |
+
import threading
|
| 40 |
+
|
| 41 |
+
def _warm():
|
| 42 |
+
logger.info("Pre-warming model pipelineβ¦")
|
| 43 |
+
models.get_pipeline()
|
| 44 |
+
logger.info("Pipeline warm-up complete.")
|
| 45 |
+
|
| 46 |
+
threading.Thread(target=_warm, daemon=True).start()
|
| 47 |
+
yield
|
| 48 |
+
logger.info("Survival Island API shutting down.")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
app = FastAPI(
|
| 53 |
+
title="Survival Island API",
|
| 54 |
+
description=(
|
| 55 |
+
"LLM-powered survival agent backend.\n"
|
| 56 |
+
"Built for the Meta PyTorch Hackathon."
|
| 57 |
+
),
|
| 58 |
+
version="1.0.0",
|
| 59 |
+
lifespan=lifespan,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
app.add_middleware(
|
| 63 |
+
CORSMiddleware,
|
| 64 |
+
allow_origins=["*"], # tighten in production if needed
|
| 65 |
+
allow_methods=["GET", "POST", "OPTIONS"],
|
| 66 |
+
allow_headers=["*"],
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# ββ Simple in-process rate limiter ββββββββββββββββββββββββββββββββββββββββββββ
|
| 71 |
+
_last_call: dict[str, float] = {}
|
| 72 |
+
_MIN_INTERVAL = 4.0 # seconds between /api/infer calls per IP
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _rate_ok(ip: str) -> bool:
|
| 76 |
+
now = time.monotonic()
|
| 77 |
+
if now - _last_call.get(ip, 0.0) < _MIN_INTERVAL:
|
| 78 |
+
return False
|
| 79 |
+
_last_call[ip] = now
|
| 80 |
+
return True
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
|
| 85 |
+
@app.get("/api/health")
|
| 86 |
+
async def health():
|
| 87 |
+
"""Liveness probe β always returns 200 while the process is running."""
|
| 88 |
+
return {
|
| 89 |
+
"status": "ok",
|
| 90 |
+
"model": os.getenv("HF_MODEL", "unset"),
|
| 91 |
+
"localPipeline": models._use_local,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.get("/", include_in_schema=False)
|
| 96 |
+
async def root():
|
| 97 |
+
"""Root info route for browser access."""
|
| 98 |
+
return {
|
| 99 |
+
"message": "Survival Island API is running.",
|
| 100 |
+
"endpoints": ["/api/health", "/api/config", "/api/infer"],
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@app.get("/api/config")
|
| 105 |
+
async def config():
|
| 106 |
+
"""Public runtime configuration for the frontend."""
|
| 107 |
+
return {
|
| 108 |
+
"model": os.getenv("HF_MODEL", "mistralai/Mistral-7B-Instruct-v0.2"),
|
| 109 |
+
"hasToken": bool(os.getenv("HF_TOKEN")),
|
| 110 |
+
"localPipeline": models._use_local,
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@app.post("/api/infer", response_model=InferResponse)
|
| 115 |
+
async def infer(state: GameState, request: Request):
|
| 116 |
+
"""
|
| 117 |
+
Accept a GameState JSON body, build the LLM prompt,
|
| 118 |
+
run inference, and return the chosen action + thought.
|
| 119 |
+
|
| 120 |
+
Inference priority: local pipeline β HF Inference API β rule fallback.
|
| 121 |
+
"""
|
| 122 |
+
ip = request.client.host if request.client else "unknown"
|
| 123 |
+
if not _rate_ok(ip):
|
| 124 |
+
raise HTTPException(
|
| 125 |
+
status_code=429,
|
| 126 |
+
detail="Too many requests β wait a few seconds.",
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
prompt = build_prompt(state)
|
| 130 |
+
logger.info(
|
| 131 |
+
f"[infer] gen={state.generation} ip={ip} "
|
| 132 |
+
f"challenge={state.activeChallenge.type if state.activeChallenge else 'none'}"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Try inference paths
|
| 136 |
+
source = "fallback"
|
| 137 |
+
try:
|
| 138 |
+
if models._use_local:
|
| 139 |
+
result = models.infer_local(prompt)
|
| 140 |
+
source = "local"
|
| 141 |
+
elif os.getenv("HF_TOKEN"):
|
| 142 |
+
result = models.infer_api(prompt)
|
| 143 |
+
source = "api"
|
| 144 |
+
else:
|
| 145 |
+
result = models.run_inference(prompt)
|
| 146 |
+
except Exception as exc:
|
| 147 |
+
logger.warning(f"[infer] Primary inference failed ({exc}), using fallback.")
|
| 148 |
+
result = models.run_inference(prompt)
|
| 149 |
+
|
| 150 |
+
return InferResponse(
|
| 151 |
+
action=result["action"],
|
| 152 |
+
thought=result["thought"],
|
| 153 |
+
source=source,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ββ Global error handler ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
+
@app.exception_handler(Exception)
|
| 159 |
+
async def _global_error(request: Request, exc: Exception):
|
| 160 |
+
logger.error(f"Unhandled error on {request.url}: {exc}", exc_info=True)
|
| 161 |
+
return JSONResponse(
|
| 162 |
+
status_code=500,
|
| 163 |
+
content={"detail": "Internal server error."},
|
| 164 |
+
)
|
server/environment.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
server/environment.py
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
Pydantic v2 models that mirror the React game state sent from the frontend,
|
| 5 |
+
plus the prompt builder that converts a GameState into the [INST] string
|
| 6 |
+
consumed by the LLM.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from typing import List, Optional
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ββ Request / Response models βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 16 |
+
|
| 17 |
+
class Inventory(BaseModel):
|
| 18 |
+
spear: bool = False
|
| 19 |
+
bow: bool = False
|
| 20 |
+
fishingRod: bool = False
|
| 21 |
+
boat: bool = False
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class BaseCamp(BaseModel):
|
| 25 |
+
x: Optional[float] = None
|
| 26 |
+
y: Optional[float] = None
|
| 27 |
+
level: int = 0
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class AIMemory(BaseModel):
|
| 31 |
+
evolutionLevel: int = 1
|
| 32 |
+
pastDeaths: List[str] = Field(default_factory=list)
|
| 33 |
+
totalGenerations: int = 0
|
| 34 |
+
challengesWon: int = 0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class ActiveChallenge(BaseModel):
|
| 38 |
+
name: str
|
| 39 |
+
type: str
|
| 40 |
+
timeLimit: int
|
| 41 |
+
maxTime: int = 0
|
| 42 |
+
progress: str = ""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class GameState(BaseModel):
|
| 46 |
+
"""Full agent snapshot sent by the frontend to /api/infer."""
|
| 47 |
+
|
| 48 |
+
# Vitals
|
| 49 |
+
health: float = Field(..., ge=0, le=100)
|
| 50 |
+
hunger: float = Field(..., ge=0, le=100)
|
| 51 |
+
thirst: float = Field(..., ge=0, le=100)
|
| 52 |
+
stamina: float = Field(..., ge=0, le=100)
|
| 53 |
+
fear: float = Field(default=0, ge=0, le=100)
|
| 54 |
+
|
| 55 |
+
# Resources
|
| 56 |
+
wood: int = 0
|
| 57 |
+
stone: int = 0
|
| 58 |
+
food: int = 0
|
| 59 |
+
water: int = 0
|
| 60 |
+
|
| 61 |
+
# World context
|
| 62 |
+
playerX: float = 1000
|
| 63 |
+
isNight: bool = False
|
| 64 |
+
activeEvents: List[str] = Field(default_factory=list)
|
| 65 |
+
predatorNear: bool = False
|
| 66 |
+
|
| 67 |
+
# Agent state
|
| 68 |
+
inventory: Inventory = Field(default_factory=Inventory)
|
| 69 |
+
baseCamp: BaseCamp = Field(default_factory=BaseCamp)
|
| 70 |
+
memory: AIMemory = Field(default_factory=AIMemory)
|
| 71 |
+
generation: int = 1
|
| 72 |
+
|
| 73 |
+
# Optional active challenge
|
| 74 |
+
activeChallenge: Optional[ActiveChallenge] = None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class InferResponse(BaseModel):
|
| 78 |
+
action: str
|
| 79 |
+
thought: str
|
| 80 |
+
source: str # "local" | "api" | "fallback"
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ββ Prompt construction βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
|
| 85 |
+
_DEATH_LESSON_MAP: dict[str, str] = {
|
| 86 |
+
"starvation": "CRITICAL: Prioritize food β starvation has killed me before.",
|
| 87 |
+
"dehydration": "CRITICAL: Prioritize water β dehydration has killed me before.",
|
| 88 |
+
"hypothermia": "CRITICAL: Seek shelter at night β hypothermia has killed me before.",
|
| 89 |
+
"heatstroke": "CRITICAL: Find shade/water during heatwaves β heatstroke has killed me before.",
|
| 90 |
+
"lion": "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
|
| 91 |
+
"mauled": "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
|
| 92 |
+
"panther": "CRITICAL: Craft a spear/bow before exploring. FLEE when predators are near until armed.",
|
| 93 |
+
"crocodile": "CRITICAL: Avoid water edges without a boat β crocodiles are deadly.",
|
| 94 |
+
"flood": "CRITICAL: During floods, evacuate eastward IMMEDIATELY.",
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
VALID_ACTIONS = (
|
| 98 |
+
"FORAGE, HUNT, FISH, GET_WATER, SEEK_SHELTER, BUILD_CAMP, UPGRADE_CAMP, "
|
| 99 |
+
"CRAFT_SPEAR, CRAFT_BOW, CRAFT_ROD, CRAFT_BOAT, EVACUATE, FIGHT, FLEE, WANDER"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _derive_lessons(past_deaths: list[str]) -> list[str]:
|
| 104 |
+
seen: set[str] = set()
|
| 105 |
+
lessons: list[str] = []
|
| 106 |
+
for death in past_deaths:
|
| 107 |
+
low = death.lower()
|
| 108 |
+
for keyword, lesson in _DEATH_LESSON_MAP.items():
|
| 109 |
+
if keyword in low and lesson not in seen:
|
| 110 |
+
seen.add(lesson)
|
| 111 |
+
lessons.append(lesson)
|
| 112 |
+
return lessons
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _strategy_label(memory: AIMemory) -> str:
|
| 116 |
+
n = len(memory.pastDeaths)
|
| 117 |
+
if n >= 5:
|
| 118 |
+
return "veteran"
|
| 119 |
+
if n >= 3:
|
| 120 |
+
return "cautious"
|
| 121 |
+
if memory.evolutionLevel > 1:
|
| 122 |
+
return "experienced"
|
| 123 |
+
return "basic"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def build_prompt(state: GameState) -> str:
|
| 127 |
+
"""
|
| 128 |
+
Convert a GameState into the [INST] prompt consumed by the LLM.
|
| 129 |
+
Mirrors the prompt built in App.jsx so server-side inference
|
| 130 |
+
produces identical quality to client-side inference.
|
| 131 |
+
"""
|
| 132 |
+
lessons = _derive_lessons(state.memory.pastDeaths)
|
| 133 |
+
strategy = _strategy_label(state.memory)
|
| 134 |
+
|
| 135 |
+
lessons_block = (
|
| 136 |
+
"\n".join(f"{i + 1}. {l}" for i, l in enumerate(lessons))
|
| 137 |
+
if lessons
|
| 138 |
+
else "No prior deaths β explore and gather resources."
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
challenge_block = ""
|
| 142 |
+
if state.activeChallenge:
|
| 143 |
+
c = state.activeChallenge
|
| 144 |
+
challenge_block = (
|
| 145 |
+
f'\nACTIVE CHALLENGE: "{c.name}" (type: {c.type}) β {c.timeLimit}s remaining.\n'
|
| 146 |
+
f"Challenge progress: {c.progress or 'just started'}.\n"
|
| 147 |
+
"Prioritize completing this challenge above all else!"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
inv = state.inventory
|
| 151 |
+
return (
|
| 152 |
+
f"<s>[INST] You are the survival instinct AI (Generation {state.generation}) "
|
| 153 |
+
f"of Subject-01. You have died {len(state.memory.pastDeaths)} times.\n\n"
|
| 154 |
+
f"STRATEGY LEVEL: {strategy.upper()}\n"
|
| 155 |
+
f"\nLESSONS FROM PAST DEATHS:\n{lessons_block}"
|
| 156 |
+
f"{challenge_block}\n\n"
|
| 157 |
+
f"Current status:\n"
|
| 158 |
+
f"HP:{state.health:.0f}, Hunger:{state.hunger:.0f}, "
|
| 159 |
+
f"Thirst:{state.thirst:.0f}, Fear:{state.fear:.0f}/100.\n"
|
| 160 |
+
f"Resources: Wood:{state.wood}, Stone:{state.stone}, "
|
| 161 |
+
f"Food:{state.food}, Water:{state.water}.\n"
|
| 162 |
+
f"Equipped: Spear:{inv.spear}, Bow:{inv.bow}, "
|
| 163 |
+
f"Rod:{inv.fishingRod}, Boat:{inv.boat}.\n"
|
| 164 |
+
f"Camp Level: {state.baseCamp.level}. Position X: {state.playerX:.0f}.\n"
|
| 165 |
+
f"Environment: {'Night' if state.isNight else 'Day'}, "
|
| 166 |
+
f"Events: {', '.join(state.activeEvents) or 'None'}.\n"
|
| 167 |
+
f"Predator Nearby: {'YES - HIGH DANGER' if state.predatorNear else 'No'}.\n"
|
| 168 |
+
f"{f'ACTIVE CHALLENGE: {state.activeChallenge.name} ({state.activeChallenge.type}) β {state.activeChallenge.timeLimit}s left' if state.activeChallenge else 'No active challenge.'}\n\n"
|
| 169 |
+
f"Valid Actions: {VALID_ACTIONS}.\n\n"
|
| 170 |
+
'Respond ONLY with a raw JSON object β no markdown, no extra text. '
|
| 171 |
+
'Example: {"action":"FORAGE","thought":"Need wood and resources"} [/INST]'
|
| 172 |
+
)
|
start.sh
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# start.sh
|
| 3 |
+
# Boots FastAPI (background) then Nginx (foreground) inside the container.
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo "ποΈ Survival Island startingβ¦"
|
| 7 |
+
echo " HF_MODEL : ${HF_MODEL:-not set}"
|
| 8 |
+
echo " HF_TOKEN : ${HF_TOKEN:+set (hidden)}"
|
| 9 |
+
echo " PORT : ${PORT:-7860}"
|
| 10 |
+
|
| 11 |
+
# ββ 1. Start FastAPI on port 8000 (background) ββββββββββββββββββββββββββββββββ
|
| 12 |
+
uvicorn server.app:app \
|
| 13 |
+
--host 127.0.0.1 \
|
| 14 |
+
--port 8000 \
|
| 15 |
+
--workers 1 \
|
| 16 |
+
--log-level info &
|
| 17 |
+
UVICORN_PID=$!
|
| 18 |
+
|
| 19 |
+
# ββ 2. Wait until FastAPI is healthy (up to 30 s) βββββββββββββββββββββββββββββ
|
| 20 |
+
echo "β³ Waiting for FastAPI health checkβ¦"
|
| 21 |
+
for i in $(seq 1 30); do
|
| 22 |
+
if curl -sf http://127.0.0.1:8000/api/health > /dev/null 2>&1; then
|
| 23 |
+
echo "β
FastAPI ready after ${i}s."
|
| 24 |
+
break
|
| 25 |
+
fi
|
| 26 |
+
sleep 1
|
| 27 |
+
done
|
| 28 |
+
|
| 29 |
+
# ββ 3. Start Nginx in foreground (keeps the container alive) ββββββββββββββββββ
|
| 30 |
+
echo "π Nginx serving on :${PORT:-7860}"
|
| 31 |
+
nginx -g "daemon off;"
|
| 32 |
+
|
| 33 |
+
# ββ 4. Cleanup on exit ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
echo "Nginx exited. Stopping uvicornβ¦"
|
| 35 |
+
kill "$UVICORN_PID" 2>/dev/null || true
|