test1978 commited on
Commit
cfad707
·
verified ·
1 Parent(s): 2a04987

Delete handler.py

Browse files
Files changed (1) hide show
  1. handler.py +0 -485
handler.py DELETED
@@ -1,485 +0,0 @@
1
- # ──────────────────────────────────────────────
2
- # handler.py
3
- #
4
- # Custom handler for HF Inference Endpoints.
5
- #
6
- # When HF Inference Endpoints detects a handler.py
7
- # in the repo root, it uses this class to serve
8
- # predictions instead of the default Transformers
9
- # pipeline.
10
- #
11
- # Expected repo layout:
12
- #
13
- # your-username/chess-model/
14
- # ├── handler.py ← this file
15
- # ├── config_model.json ← lists your Python modules + main
16
- # ├── config_data.json ← lists data files + zone_db key
17
- # ├── chess_mcvs.py ← your UCT search / eval module
18
- # └── requirements.txt ← runtime deps (numpy, huggingface_hub)
19
- #
20
- # config_model.json example:
21
- #
22
- # {
23
- # "files": ["matrix_model.py", "abc_model.py", "chess_mcvs.py"],
24
- # "main": "chess_mcvs"
25
- # }
26
- #
27
- # config_data.json example:
28
- #
29
- # {
30
- # "files": ["chess_zone_db.npz"],
31
- # "zone_db": "chess_zone_db.npz"
32
- # }
33
- #
34
- # Set the HF_DATA_REPO_ID environment variable to
35
- # the HF dataset repo holding your data files.
36
- #
37
- # Request format (sent by Agladiator):
38
- #
39
- # POST /
40
- # {
41
- # "inputs": {
42
- # "fen": "BBBBBBBB/BBBBBBBB/8/8/8/8/WWWWWWWW/WWWWWWWW w",
43
- # "player": "w",
44
- # "game_type": "chess"
45
- # }
46
- # }
47
- #
48
- # Response format (what you must return):
49
- #
50
- # {"move": "a2a3"}
51
- #
52
- # ──────────────────────────────────────────────
53
- from __future__ import annotations
54
-
55
- import importlib.util
56
- import json
57
- import logging
58
- import os
59
- import random
60
- import sys
61
- from pathlib import Path
62
-
63
- import numpy as np
64
-
65
- log = logging.getLogger(__name__)
66
-
67
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
- # Chess board constants (used by the minimal legal-move generator)
69
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
- BOARD_SIZE = 8
71
- _FILES = "abcdefgh"
72
- _RANKS = "12345678"
73
- WHITE, BLACK = "w", "b"
74
- EMPTY = "."
75
-
76
-
77
- class EndpointHandler:
78
- """Custom handler for chess AI on HF Inference Endpoints.
79
-
80
- Lifecycle:
81
- 1. ``__init__`` is called once when the endpoint container starts.
82
- Use it to load your model artifacts (modules, zone DB, etc.).
83
- 2. ``__call__`` is called for every inference request.
84
- It receives the parsed JSON body and must return a dict.
85
- """
86
-
87
- def __init__(self, path: str):
88
- """Load model artefacts from *path* (the cloned repo directory).
89
-
90
- Steps:
91
- 1. Read ``config_model.json`` to discover module files and main module.
92
- 2. Dynamically import each listed module.
93
- 3. Read ``config_data.json``, download data files, load zone DB.
94
- 4. Initialise the search engine.
95
- """
96
- self.path = Path(path)
97
- self.predict_fn = None
98
- self.zone_db = None
99
- self.module = None
100
-
101
- # ── 1. Read config_model.json ───────────
102
- model_config_path = self.path / "config_model.json"
103
- if not model_config_path.exists():
104
- log.warning("No config_model.json found in %s", path)
105
- model_config: dict = {}
106
- else:
107
- with open(model_config_path) as fh:
108
- model_config = json.load(fh)
109
-
110
- main_module_name = model_config.get("main", "")
111
-
112
- # ── 2. Import listed modules ────────────
113
- # Add repo dir to sys.path so relative imports work.
114
- if str(self.path) not in sys.path:
115
- sys.path.insert(0, str(self.path))
116
-
117
- for mod_file in model_config.get("files", []):
118
- mod_name = mod_file.removesuffix(".py")
119
- mod_path = self.path / mod_file
120
- if not mod_path.exists():
121
- log.warning("Module file %s not found — skipping.", mod_file)
122
- continue
123
- spec = importlib.util.spec_from_file_location(mod_name, str(mod_path))
124
- mod = importlib.util.module_from_spec(spec)
125
- sys.modules[mod_name] = mod
126
- spec.loader.exec_module(mod)
127
- log.info("Loaded module: %s", mod_name)
128
- # Mark the module matching "main" as the primary module.
129
- if mod_name == main_module_name:
130
- self.module = mod
131
-
132
- # ── 3. Read config_data.json & load zone DB ──
133
- data_config_path = self.path / "config_data.json"
134
- if not data_config_path.exists():
135
- log.warning("No config_data.json found in %s", path)
136
- data_config: dict = {}
137
- else:
138
- with open(data_config_path) as fh:
139
- data_config = json.load(fh)
140
-
141
- data_repo = os.environ.get("HF_DATA_REPO_ID", "")
142
- downloaded_files: dict[str, str] = {}
143
-
144
- if data_repo:
145
- try:
146
- from huggingface_hub import hf_hub_download
147
- except Exception:
148
- hf_hub_download = None
149
- log.warning(
150
- "huggingface_hub not installed — cannot download data files. Add it to requirements.txt"
151
- )
152
-
153
- if hf_hub_download is not None:
154
- for data_file in data_config.get("files", []):
155
- try:
156
- local_path = hf_hub_download(
157
- repo_id=data_repo,
158
- filename=data_file,
159
- repo_type="dataset",
160
- )
161
- downloaded_files[data_file] = local_path
162
- log.info("Downloaded %s from %s", data_file, data_repo)
163
- except Exception:
164
- log.warning(
165
- "Failed to download %s from %s — skipping.",
166
- data_file,
167
- data_repo,
168
- )
169
- else:
170
- log.warning(
171
- "HF_DATA_REPO_ID not set — skipping data file downloads."
172
- )
173
-
174
- zone_db_filename = data_config.get("zone_db", "")
175
- if zone_db_filename and zone_db_filename in downloaded_files:
176
- self.zone_db = np.load(
177
- downloaded_files[zone_db_filename], allow_pickle=True
178
- )
179
- log.info("Zone DB loaded from data repo %s", data_repo)
180
- elif zone_db_filename:
181
- local_zone = self.path / zone_db_filename
182
- if local_zone.exists():
183
- self.zone_db = np.load(str(local_zone), allow_pickle=True)
184
- log.info("Zone DB loaded from local file %s", local_zone)
185
- else:
186
- log.warning("No zone DB found for '%s'.", zone_db_filename)
187
-
188
- # ── 4. Initialise the predictor ─────────
189
- self._init_predictor()
190
- log.info("EndpointHandler initialised successfully.")
191
-
192
- # ──────────────────────────────────────────
193
- # Predictor initialisation
194
- # ──────────────────────────────────────────
195
- def _init_predictor(self):
196
- """Discover and configure the prediction function from the
197
- loaded module. Tries several common patterns:
198
-
199
- 1. ``module.get_move(fen, player, zone_db=...)``
200
- 2. ``module.UCTSearcher(zone_db=...).search(fen, player)``
201
- 3. ``module.predict(fen, player)``
202
- """
203
- if self.module is None:
204
- log.warning("No module loaded — predictions will use random fallback.")
205
- return
206
-
207
- # Pattern 1: module exposes a top-level get_move()
208
- if hasattr(self.module, "get_move"):
209
- raw_fn = self.module.get_move
210
- self.predict_fn = lambda fen, player: raw_fn(
211
- fen, player, zone_db=self.zone_db
212
- )
213
- log.info("Using module.get_move() for predictions.")
214
- return
215
-
216
- # Pattern 2: module exposes a UCTSearcher class
217
- if hasattr(self.module, "UCTSearcher"):
218
- searcher_cls = self.module.UCTSearcher
219
- kwargs = {}
220
- if self.zone_db is not None:
221
- kwargs["zone_db"] = self.zone_db
222
- searcher = searcher_cls(**kwargs)
223
- self.predict_fn = lambda fen, player: searcher.search(fen, player)
224
- log.info("Using module.UCTSearcher for predictions.")
225
- return
226
-
227
- # Pattern 3: module exposes a predict()
228
- if hasattr(self.module, "predict"):
229
- raw_fn = self.module.predict
230
- self.predict_fn = lambda fen, player: raw_fn(fen, player)
231
- log.info("Using module.predict() for predictions.")
232
- return
233
-
234
- log.warning(
235
- "Module %s has no get_move / UCTSearcher / predict — "
236
- "predictions will use random fallback.",
237
- self.module.__name__,
238
- )
239
-
240
- # ──────────────────────────────────────────
241
- # Inference entry point
242
- # ──────────────────────────────────────────
243
- def __call__(self, data: dict) -> dict:
244
- """Handle an inference request.
245
-
246
- Expected *data*::
247
-
248
- {"inputs": {"fen": "...", "player": "w"}}
249
-
250
- Returns::
251
-
252
- {"move": "a2a3"}
253
-
254
- Falls back to a random legal move if the model fails.
255
- """
256
- inputs = data.get("inputs", data)
257
- fen = inputs.get("fen", "")
258
- player = inputs.get("player", WHITE)
259
-
260
- if not fen:
261
- return {"error": "Missing 'fen' in request."}
262
-
263
- # Try model prediction
264
- if self.predict_fn is not None:
265
- try:
266
- move = self.predict_fn(fen, player)
267
- if move and isinstance(move, str):
268
- return {"move": move}
269
- except Exception:
270
- log.exception("Model prediction failed for FEN=%s", fen)
271
-
272
- # Fallback: random legal move
273
- moves = self._legal_moves(fen)
274
- if not moves:
275
- return {"error": "No legal moves available.", "move": "0000"}
276
-
277
- # Prefer captures (diagonal moves in chess)
278
- captures = [m for m in moves if m[0] != m[2]]
279
- move = random.choice(captures) if captures else random.choice(moves)
280
- return {"move": move}
281
-
282
- # ──────────────────────────────────────────
283
- # Minimal legal-move generator (self-contained)
284
- # ──────────────────────────────────────────
285
- @staticmethod
286
- def _legal_moves(fen: str) -> list[str]:
287
- """Return a list of pseudo-legal UCI moves for a chess FEN.
288
-
289
- This minimal generator supports pawn moves (including promotions
290
- and en-passant), knights, bishops, rooks, queens, kings and
291
- basic castling availability. It does not perform check detection
292
- and therefore may return some pseudo-legal moves in checked positions.
293
- """
294
- parts = fen.strip().split()
295
- board_part = parts[0] if parts else ""
296
- turn = parts[1] if len(parts) > 1 else WHITE
297
- castling = parts[2] if len(parts) > 2 else "-"
298
- ep_square = parts[3] if len(parts) > 3 else "-"
299
-
300
- # Parse FEN → grid (8 ranks, top (rank 8) → bottom (rank 1))
301
- grid: list[list[str]] = []
302
- for rank_str in board_part.split("/"):
303
- row: list[str] = []
304
- for ch in rank_str:
305
- if ch.isdigit():
306
- row.extend([EMPTY] * int(ch))
307
- else:
308
- row.append(ch)
309
- # normalize row length
310
- if len(row) != BOARD_SIZE:
311
- row = (row + [EMPTY] * BOARD_SIZE)[:BOARD_SIZE]
312
- grid.append(row)
313
- while len(grid) < BOARD_SIZE:
314
- grid.append([EMPTY] * BOARD_SIZE)
315
-
316
- side_white = turn == WHITE
317
-
318
- def is_enemy(ch: str) -> bool:
319
- return ch != EMPTY and ch.isupper() != side_white
320
-
321
- def is_own(ch: str) -> bool:
322
- return ch != EMPTY and ch.isupper() == side_white
323
-
324
- moves: list[str] = []
325
-
326
- # En-passant target square (if any)
327
- ep_r = ep_c = None
328
- if ep_square and ep_square != "-":
329
- try:
330
- ep_c = _FILES.index(ep_square[0])
331
- ep_r = BOARD_SIZE - int(ep_square[1])
332
- except Exception:
333
- ep_r = ep_c = None
334
-
335
- start_row = 6 if side_white else 1
336
- last_row = 0 if side_white else 7
337
- direction = -1 if side_white else 1
338
-
339
- for r in range(BOARD_SIZE):
340
- for c in range(BOARD_SIZE):
341
- ch = grid[r][c]
342
- if ch == EMPTY:
343
- continue
344
- if ch.isupper() != side_white:
345
- continue
346
- piece = ch.lower()
347
- from_sq = f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - r]}"
348
-
349
- if piece == "p":
350
- nr = r + direction
351
- # forward one
352
- if 0 <= nr < BOARD_SIZE and grid[nr][c] == EMPTY:
353
- # promotion
354
- if nr == last_row:
355
- for promo in ("q", "r", "b", "n"):
356
- moves.append(
357
- f"{from_sq}{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - nr]}{promo}"
358
- )
359
- else:
360
- moves.append(
361
- f"{from_sq}{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - nr]}"
362
- )
363
- # double move from start
364
- if r == start_row:
365
- nr2 = r + 2 * direction
366
- if 0 <= nr2 < BOARD_SIZE and grid[nr2][c] == EMPTY:
367
- moves.append(
368
- f"{from_sq}{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - nr2]}"
369
- )
370
- # captures (including en-passant)
371
- for dc in (-1, 1):
372
- nc = c + dc
373
- if nc < 0 or nc >= BOARD_SIZE:
374
- continue
375
- if 0 <= nr < BOARD_SIZE:
376
- target = grid[nr][nc]
377
- if is_enemy(target):
378
- if nr == last_row:
379
- for promo in ("q", "r", "b", "n"):
380
- moves.append(
381
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}{promo}"
382
- )
383
- else:
384
- moves.append(
385
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
386
- )
387
- # en-passant capture
388
- if ep_r is not None and ep_r == nr and ep_c == nc:
389
- cap_r = r
390
- cap_c = nc
391
- if (
392
- 0 <= cap_r < BOARD_SIZE
393
- and grid[cap_r][cap_c].lower() == "p"
394
- and is_enemy(grid[cap_r][cap_c])
395
- ):
396
- moves.append(
397
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
398
- )
399
-
400
- elif piece == "n":
401
- for dr, dc in (
402
- (-2, -1),
403
- (-2, 1),
404
- (-1, -2),
405
- (-1, 2),
406
- (1, -2),
407
- (1, 2),
408
- (2, -1),
409
- (2, 1),
410
- ):
411
- nr = r + dr
412
- nc = c + dc
413
- if 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE:
414
- target = grid[nr][nc]
415
- if not is_own(target):
416
- moves.append(
417
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
418
- )
419
-
420
- elif piece in ("b", "r", "q"):
421
- directions = []
422
- if piece in ("b", "q"):
423
- directions += [(-1, -1), (-1, 1), (1, -1), (1, 1)]
424
- if piece in ("r", "q"):
425
- directions += [(-1, 0), (1, 0), (0, -1), (0, 1)]
426
- for dr, dc in directions:
427
- nr = r + dr
428
- nc = c + dc
429
- while 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE:
430
- target = grid[nr][nc]
431
- if target == EMPTY:
432
- moves.append(
433
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
434
- )
435
- else:
436
- if is_enemy(target):
437
- moves.append(
438
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
439
- )
440
- break
441
- nr += dr
442
- nc += dc
443
-
444
- elif piece == "k":
445
- for dr in (-1, 0, 1):
446
- for dc in (-1, 0, 1):
447
- if dr == 0 and dc == 0:
448
- continue
449
- nr = r + dr
450
- nc = c + dc
451
- if 0 <= nr < BOARD_SIZE and 0 <= nc < BOARD_SIZE:
452
- target = grid[nr][nc]
453
- if not is_own(target):
454
- moves.append(
455
- f"{from_sq}{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
456
- )
457
- # Basic castling checks (does not verify check conditions)
458
- if side_white:
459
- if "K" in castling and r == 7 and c == 4:
460
- if grid[7][5] == EMPTY and grid[7][6] == EMPTY and grid[7][7] != EMPTY and grid[7][7].isupper():
461
- moves.append(f"{from_sq}g1")
462
- if "Q" in castling and r == 7 and c == 4:
463
- if (
464
- grid[7][3] == EMPTY
465
- and grid[7][2] == EMPTY
466
- and grid[7][1] == EMPTY
467
- and grid[7][0] != EMPTY
468
- and grid[7][0].isupper()
469
- ):
470
- moves.append(f"{from_sq}c1")
471
- else:
472
- if "k" in castling and r == 0 and c == 4:
473
- if grid[0][5] == EMPTY and grid[0][6] == EMPTY and grid[0][7] != EMPTY and grid[0][7].islower():
474
- moves.append(f"{from_sq}g8")
475
- if "q" in castling and r == 0 and c == 4:
476
- if (
477
- grid[0][3] == EMPTY
478
- and grid[0][2] == EMPTY
479
- and grid[0][1] == EMPTY
480
- and grid[0][0] != EMPTY
481
- and grid[0][0].islower()
482
- ):
483
- moves.append(f"{from_sq}c8")
484
-
485
- return moves