Update app.py
Browse files
app.py
CHANGED
|
@@ -18,18 +18,15 @@ class NexusNanoEngine:
|
|
| 18 |
def __init__(self, model_path: str):
|
| 19 |
if not os.path.exists(model_path):
|
| 20 |
raise FileNotFoundError(f"Model not found: {model_path}")
|
| 21 |
-
|
| 22 |
-
logger.info(f"Loading: {model_path} ({os.path.getsize(model_path)/(1024*1024):.2f} MB)")
|
| 23 |
-
|
| 24 |
sess_options = ort.SessionOptions()
|
| 25 |
sess_options.intra_op_num_threads = 2
|
| 26 |
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 27 |
-
|
| 28 |
self.session = ort.InferenceSession(model_path, sess_options=sess_options, providers=['CPUExecutionProvider'])
|
| 29 |
self.input_name = self.session.get_inputs()[0].name
|
| 30 |
self.output_name = self.session.get_outputs()[0].name
|
| 31 |
self.nodes = 0
|
| 32 |
-
logger.info("β
|
| 33 |
|
| 34 |
def fen_to_tensor(self, fen: str) -> np.ndarray:
|
| 35 |
board = chess.Board(fen)
|
|
@@ -54,18 +51,15 @@ class NexusNanoEngine:
|
|
| 54 |
s = 0
|
| 55 |
if board.is_capture(m):
|
| 56 |
v, a = board.piece_at(m.to_square), board.piece_at(m.from_square)
|
| 57 |
-
if v and a:
|
| 58 |
-
s = self.PIECE_VALUES.get(v.piece_type, 0) * 10 - self.PIECE_VALUES.get(a.piece_type, 0)
|
| 59 |
if m.promotion == chess.QUEEN: s += 90
|
| 60 |
scored.append((s, m))
|
| 61 |
scored.sort(key=lambda x: x[0], reverse=True)
|
| 62 |
return [m for _, m in scored]
|
| 63 |
|
| 64 |
-
def alpha_beta(self, board, depth, alpha, beta)
|
| 65 |
-
if board.is_game_over():
|
| 66 |
-
|
| 67 |
-
if depth == 0:
|
| 68 |
-
return self.evaluate(board), None
|
| 69 |
moves = list(board.legal_moves)
|
| 70 |
if not moves: return 0, None
|
| 71 |
moves = self.order_moves(board, moves)
|
|
@@ -75,8 +69,7 @@ class NexusNanoEngine:
|
|
| 75 |
score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha)
|
| 76 |
score = -score
|
| 77 |
board.pop()
|
| 78 |
-
if score > best_score:
|
| 79 |
-
best_score, best_move = score, move
|
| 80 |
alpha = max(alpha, score)
|
| 81 |
if alpha >= beta: break
|
| 82 |
return best_score, best_move
|
|
@@ -85,16 +78,13 @@ class NexusNanoEngine:
|
|
| 85 |
board = chess.Board(fen)
|
| 86 |
self.nodes = 0
|
| 87 |
moves = list(board.legal_moves)
|
| 88 |
-
if
|
| 89 |
-
|
| 90 |
-
if len(moves) == 1:
|
| 91 |
-
return {'best_move': moves[0].uci(), 'evaluation': round(self.evaluate(board)/100, 2), 'nodes': 1, 'depth': 0}
|
| 92 |
best_move, best_score, current_depth = moves[0], float('-inf'), 1
|
| 93 |
for d in range(1, depth + 1):
|
| 94 |
try:
|
| 95 |
score, move = self.alpha_beta(board, d, float('-inf'), float('inf'))
|
| 96 |
-
if move:
|
| 97 |
-
best_move, best_score, current_depth = move, score, d
|
| 98 |
except: break
|
| 99 |
return {'best_move': best_move.uci(), 'evaluation': round(best_score/100, 2), 'depth': current_depth, 'nodes': self.nodes}
|
| 100 |
|
|
@@ -117,55 +107,33 @@ class MoveResponse(BaseModel):
|
|
| 117 |
@app.on_event("startup")
|
| 118 |
async def startup():
|
| 119 |
global engine
|
| 120 |
-
logger.info("π Starting Nexus-Nano...")
|
| 121 |
-
|
| 122 |
-
# FIXED: Check both possible paths
|
| 123 |
-
possible_paths = [
|
| 124 |
-
"/app/app/models/nexus-nano.onnx", # When uploaded to app/models/
|
| 125 |
-
"/app/models/nexus-nano.onnx" # When uploaded to models/
|
| 126 |
-
]
|
| 127 |
-
|
| 128 |
-
model_path = None
|
| 129 |
-
for path in possible_paths:
|
| 130 |
-
if os.path.exists(path):
|
| 131 |
-
model_path = path
|
| 132 |
-
logger.info(f"β
Found model at: {path}")
|
| 133 |
-
break
|
| 134 |
-
|
| 135 |
-
if not model_path:
|
| 136 |
-
logger.error("β Model not found in any expected location")
|
| 137 |
-
logger.error(f"Checked paths: {possible_paths}")
|
| 138 |
-
# List all files
|
| 139 |
-
for root, dirs, files in os.walk("/app"):
|
| 140 |
-
for file in files:
|
| 141 |
-
if file.endswith('.onnx'):
|
| 142 |
-
logger.error(f"Found .onnx at: {os.path.join(root, file)}")
|
| 143 |
-
raise FileNotFoundError("Model not found")
|
| 144 |
-
|
| 145 |
try:
|
| 146 |
engine = NexusNanoEngine(model_path)
|
|
|
|
| 147 |
except Exception as e:
|
| 148 |
-
logger.error(f"β
|
| 149 |
raise
|
| 150 |
|
| 151 |
@app.get("/health")
|
| 152 |
async def health():
|
| 153 |
-
return {"status": "healthy" if engine else "unhealthy", "
|
| 154 |
|
| 155 |
@app.post("/get-move", response_model=MoveResponse)
|
| 156 |
async def get_move(req: MoveRequest):
|
| 157 |
-
if not engine: raise HTTPException(503, "
|
| 158 |
try: chess.Board(req.fen)
|
| 159 |
except: raise HTTPException(400, "Invalid FEN")
|
| 160 |
start = time.time()
|
| 161 |
try:
|
| 162 |
result = engine.search(req.fen, req.depth)
|
| 163 |
elapsed = int((time.time() - start) * 1000)
|
| 164 |
-
logger.info(f"
|
| 165 |
return MoveResponse(best_move=result['best_move'], evaluation=result['evaluation'],
|
| 166 |
depth_searched=result['depth'], nodes_evaluated=result['nodes'], time_taken=elapsed)
|
| 167 |
except Exception as e:
|
| 168 |
-
logger.error(f"Error: {e}"
|
| 169 |
raise HTTPException(500, str(e))
|
| 170 |
|
| 171 |
@app.get("/")
|
|
|
|
| 18 |
def __init__(self, model_path: str):
|
| 19 |
if not os.path.exists(model_path):
|
| 20 |
raise FileNotFoundError(f"Model not found: {model_path}")
|
| 21 |
+
logger.info(f"Loading model from {model_path}...")
|
|
|
|
|
|
|
| 22 |
sess_options = ort.SessionOptions()
|
| 23 |
sess_options.intra_op_num_threads = 2
|
| 24 |
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
|
|
|
| 25 |
self.session = ort.InferenceSession(model_path, sess_options=sess_options, providers=['CPUExecutionProvider'])
|
| 26 |
self.input_name = self.session.get_inputs()[0].name
|
| 27 |
self.output_name = self.session.get_outputs()[0].name
|
| 28 |
self.nodes = 0
|
| 29 |
+
logger.info("β
Nexus-Nano engine loaded")
|
| 30 |
|
| 31 |
def fen_to_tensor(self, fen: str) -> np.ndarray:
|
| 32 |
board = chess.Board(fen)
|
|
|
|
| 51 |
s = 0
|
| 52 |
if board.is_capture(m):
|
| 53 |
v, a = board.piece_at(m.to_square), board.piece_at(m.from_square)
|
| 54 |
+
if v and a: s = self.PIECE_VALUES.get(v.piece_type, 0) * 10 - self.PIECE_VALUES.get(a.piece_type, 0)
|
|
|
|
| 55 |
if m.promotion == chess.QUEEN: s += 90
|
| 56 |
scored.append((s, m))
|
| 57 |
scored.sort(key=lambda x: x[0], reverse=True)
|
| 58 |
return [m for _, m in scored]
|
| 59 |
|
| 60 |
+
def alpha_beta(self, board, depth, alpha, beta):
|
| 61 |
+
if board.is_game_over(): return (-10000 if board.is_checkmate() else 0), None
|
| 62 |
+
if depth == 0: return self.evaluate(board), None
|
|
|
|
|
|
|
| 63 |
moves = list(board.legal_moves)
|
| 64 |
if not moves: return 0, None
|
| 65 |
moves = self.order_moves(board, moves)
|
|
|
|
| 69 |
score, _ = self.alpha_beta(board, depth - 1, -beta, -alpha)
|
| 70 |
score = -score
|
| 71 |
board.pop()
|
| 72 |
+
if score > best_score: best_score, best_move = score, move
|
|
|
|
| 73 |
alpha = max(alpha, score)
|
| 74 |
if alpha >= beta: break
|
| 75 |
return best_score, best_move
|
|
|
|
| 78 |
board = chess.Board(fen)
|
| 79 |
self.nodes = 0
|
| 80 |
moves = list(board.legal_moves)
|
| 81 |
+
if not moves: return {'best_move': '0000', 'evaluation': 0.0, 'nodes': 0, 'depth': 0}
|
| 82 |
+
if len(moves) == 1: return {'best_move': moves[0].uci(), 'evaluation': round(self.evaluate(board)/100, 2), 'nodes': 1, 'depth': 0}
|
|
|
|
|
|
|
| 83 |
best_move, best_score, current_depth = moves[0], float('-inf'), 1
|
| 84 |
for d in range(1, depth + 1):
|
| 85 |
try:
|
| 86 |
score, move = self.alpha_beta(board, d, float('-inf'), float('inf'))
|
| 87 |
+
if move: best_move, best_score, current_depth = move, score, d
|
|
|
|
| 88 |
except: break
|
| 89 |
return {'best_move': best_move.uci(), 'evaluation': round(best_score/100, 2), 'depth': current_depth, 'nodes': self.nodes}
|
| 90 |
|
|
|
|
| 107 |
@app.on_event("startup")
|
| 108 |
async def startup():
|
| 109 |
global engine
|
| 110 |
+
logger.info("π Starting Nexus-Nano API...")
|
| 111 |
+
model_path = "/app/models/nexus-nano.onnx"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
try:
|
| 113 |
engine = NexusNanoEngine(model_path)
|
| 114 |
+
logger.info("β
Engine ready")
|
| 115 |
except Exception as e:
|
| 116 |
+
logger.error(f"β Failed to load engine: {e}")
|
| 117 |
raise
|
| 118 |
|
| 119 |
@app.get("/health")
|
| 120 |
async def health():
|
| 121 |
+
return {"status": "healthy" if engine else "unhealthy", "model_loaded": engine is not None, "version": "1.0.0"}
|
| 122 |
|
| 123 |
@app.post("/get-move", response_model=MoveResponse)
|
| 124 |
async def get_move(req: MoveRequest):
|
| 125 |
+
if not engine: raise HTTPException(503, "Engine not loaded")
|
| 126 |
try: chess.Board(req.fen)
|
| 127 |
except: raise HTTPException(400, "Invalid FEN")
|
| 128 |
start = time.time()
|
| 129 |
try:
|
| 130 |
result = engine.search(req.fen, req.depth)
|
| 131 |
elapsed = int((time.time() - start) * 1000)
|
| 132 |
+
logger.info(f"Move: {result['best_move']} | Eval: {result['evaluation']:+.2f} | Time: {elapsed}ms")
|
| 133 |
return MoveResponse(best_move=result['best_move'], evaluation=result['evaluation'],
|
| 134 |
depth_searched=result['depth'], nodes_evaluated=result['nodes'], time_taken=elapsed)
|
| 135 |
except Exception as e:
|
| 136 |
+
logger.error(f"Error: {e}")
|
| 137 |
raise HTTPException(500, str(e))
|
| 138 |
|
| 139 |
@app.get("/")
|