test1978 commited on
Commit
6d10731
·
verified ·
1 Parent(s): 3dd414c

Remove handler.py - using official server handler instead

Browse files
Files changed (1) hide show
  1. handler.py +0 -320
handler.py DELETED
@@ -1,320 +0,0 @@
1
- # ──────────────────────────────────────────────
2
- # sample_breakthrough_handler.py
3
- #
4
- # Sample custom handler for HF Inference Endpoints.
5
- #
6
- # *** COPY THIS FILE INTO YOUR BREAKTHROUGH HF
7
- # MODEL REPO AS ``handler.py``. ***
8
- #
9
- # When HF Inference Endpoints detects a handler.py
10
- # in the repo root, it uses this class to serve
11
- # predictions instead of the default Transformers
12
- # pipeline.
13
- #
14
- # Expected repo layout:
15
- #
16
- # your-username/breakthrough-model/
17
- # ├── handler.py ← this file (rename to handler.py)
18
- # ├── config_model.json ← lists your Python modules
19
- # ├── breakthrough_mcvs.py ← your UCT search / eval module
20
- # └── requirements.txt ← runtime deps (numpy, huggingface_hub)
21
- #
22
- # And if you have a separate data repo for the zone DB:
23
- #
24
- # your-username/breakthrough-data/
25
- # └── zone_db.npz
26
- #
27
- # config_model.json example:
28
- #
29
- # {
30
- # "modules": ["breakthrough_mcvs.py"],
31
- # "data_repo_id": "your-username/breakthrough-data",
32
- # "zone_db_filename": "zone_db.npz"
33
- # }
34
- #
35
- # If the zone DB lives in your model repo instead
36
- # of a separate data repo, omit "data_repo_id" and
37
- # place zone_db.npz alongside handler.py.
38
- #
39
- # Request format (sent by Agladiator):
40
- #
41
- # POST /
42
- # {
43
- # "inputs": {
44
- # "fen": "BBBBBBBB/BBBBBBBB/8/8/8/8/WWWWWWWW/WWWWWWWW w",
45
- # "player": "w",
46
- # "game_type": "breakthrough"
47
- # }
48
- # }
49
- #
50
- # Response format (what you must return):
51
- #
52
- # {"move": "a2a3"}
53
- #
54
- # ──────────────────────────────────────────────
55
- from __future__ import annotations
56
-
57
- import importlib.util
58
- import json
59
- import logging
60
- import os
61
- import random
62
- import sys
63
- from pathlib import Path
64
-
65
- import numpy as np
66
-
67
- log = logging.getLogger(__name__)
68
-
69
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
- # Breakthrough board constants (must match the
71
- # server's breakthrough_engine.py)
72
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
- BOARD_SIZE = 8
74
- _FILES = "abcdefgh"
75
- _RANKS = "12345678"
76
- WHITE, BLACK = "w", "b"
77
- WHITE_PIECE, BLACK_PIECE, EMPTY = "W", "B", "."
78
-
79
-
80
- class EndpointHandler:
81
- """Custom handler for Breakthrough AI on HF Inference Endpoints.
82
-
83
- Lifecycle:
84
- 1. ``__init__`` is called once when the endpoint container starts.
85
- Use it to load your model artifacts (modules, zone DB, etc.).
86
- 2. ``__call__`` is called for every inference request.
87
- It receives the parsed JSON body and must return a dict.
88
- """
89
-
90
- def __init__(self, path: str):
91
- """Load model artefacts from *path* (the cloned repo directory).
92
-
93
- Steps:
94
- 1. Read ``config_model.json`` to discover module files.
95
- 2. Dynamically import each listed module.
96
- 3. Download / load the zone database.
97
- 4. Initialise the search engine.
98
- """
99
- self.path = Path(path)
100
- self.predict_fn = None
101
- self.zone_db = None
102
- self.module = None
103
-
104
- # ── 1. Read config ──────────────────────
105
- config_path = self.path / "config_model.json"
106
- if not config_path.exists():
107
- log.warning("No config_model.json found in %s", path)
108
- self.config: dict = {}
109
- else:
110
- with open(config_path) as fh:
111
- self.config = json.load(fh)
112
-
113
- # ── 2. Import listed modules ────────────
114
- # Add repo dir to sys.path so relative imports work.
115
- if str(self.path) not in sys.path:
116
- sys.path.insert(0, str(self.path))
117
-
118
- for mod_file in self.config.get("modules", []):
119
- mod_name = mod_file.removesuffix(".py")
120
- mod_path = self.path / mod_file
121
- if not mod_path.exists():
122
- log.warning("Module file %s not found — skipping.", mod_file)
123
- continue
124
- spec = importlib.util.spec_from_file_location(mod_name, str(mod_path))
125
- mod = importlib.util.module_from_spec(spec)
126
- sys.modules[mod_name] = mod
127
- spec.loader.exec_module(mod)
128
- # Keep the last loaded module as the "main" module.
129
- self.module = mod
130
- log.info("Loaded module: %s", mod_name)
131
-
132
- # ── 3. Load zone database ───────────────
133
- zone_db_filename = self.config.get("zone_db_filename", "zone_db.npz")
134
-
135
- # Check env var first (set by Agladiator platform), then config.
136
- data_repo = os.environ.get(
137
- "HF_DATA_REPO_ID",
138
- self.config.get("data_repo_id", ""),
139
- )
140
-
141
- if data_repo:
142
- # Download from the linked HF dataset repo.
143
- try:
144
- from huggingface_hub import hf_hub_download
145
-
146
- zone_path = hf_hub_download(
147
- repo_id=data_repo,
148
- filename=zone_db_filename,
149
- repo_type="dataset",
150
- )
151
- self.zone_db = np.load(zone_path, allow_pickle=True)
152
- log.info("Zone DB loaded from data repo %s", data_repo)
153
- except Exception:
154
- log.exception(
155
- "Failed to download zone DB from %s — "
156
- "falling back to local file.",
157
- data_repo,
158
- )
159
-
160
- # Fall back to a local copy in the model repo.
161
- if self.zone_db is None:
162
- local_zone = self.path / zone_db_filename
163
- if local_zone.exists():
164
- self.zone_db = np.load(str(local_zone), allow_pickle=True)
165
- log.info("Zone DB loaded from local file %s", local_zone)
166
- else:
167
- log.warning(
168
- "No zone DB found (checked data repo '%s' and local '%s').",
169
- data_repo,
170
- local_zone,
171
- )
172
-
173
- # ── 4. Initialise the predictor ─────────
174
- self._init_predictor()
175
- log.info("EndpointHandler initialised successfully.")
176
-
177
- # ──────────────────────────────────────────
178
- # Predictor initialisation
179
- # ──────────────────────────────────────────
180
- def _init_predictor(self):
181
- """Discover and configure the prediction function from the
182
- loaded module. Tries several common patterns:
183
-
184
- 1. ``module.get_move(fen, player, zone_db=...)``
185
- 2. ``module.UCTSearcher(zone_db=...).search(fen, player)``
186
- 3. ``module.predict(fen, player)``
187
- """
188
- if self.module is None:
189
- log.warning("No module loaded — predictions will use random fallback.")
190
- return
191
-
192
- # Pattern 1: module exposes a top-level get_move()
193
- if hasattr(self.module, "get_move"):
194
- raw_fn = self.module.get_move
195
- self.predict_fn = lambda fen, player: raw_fn(
196
- fen, player, zone_db=self.zone_db
197
- )
198
- log.info("Using module.get_move() for predictions.")
199
- return
200
-
201
- # Pattern 2: module exposes a UCTSearcher class
202
- if hasattr(self.module, "UCTSearcher"):
203
- searcher_cls = self.module.UCTSearcher
204
- kwargs = {}
205
- if self.zone_db is not None:
206
- kwargs["zone_db"] = self.zone_db
207
- searcher = searcher_cls(**kwargs)
208
- self.predict_fn = lambda fen, player: searcher.search(fen, player)
209
- log.info("Using module.UCTSearcher for predictions.")
210
- return
211
-
212
- # Pattern 3: module exposes a predict()
213
- if hasattr(self.module, "predict"):
214
- raw_fn = self.module.predict
215
- self.predict_fn = lambda fen, player: raw_fn(fen, player)
216
- log.info("Using module.predict() for predictions.")
217
- return
218
-
219
- log.warning(
220
- "Module %s has no get_move / UCTSearcher / predict — "
221
- "predictions will use random fallback.",
222
- self.module.__name__,
223
- )
224
-
225
- # ──────────────────────────────────────────
226
- # Inference entry point
227
- # ──────────────────────────────────────────
228
- def __call__(self, data: dict) -> dict:
229
- """Handle an inference request.
230
-
231
- Expected *data*::
232
-
233
- {"inputs": {"fen": "...", "player": "w"}}
234
-
235
- Returns::
236
-
237
- {"move": "a2a3"}
238
-
239
- Falls back to a random legal move if the model fails.
240
- """
241
- inputs = data.get("inputs", data)
242
- fen = inputs.get("fen", "")
243
- player = inputs.get("player", WHITE)
244
-
245
- if not fen:
246
- return {"error": "Missing 'fen' in request."}
247
-
248
- # Try model prediction
249
- if self.predict_fn is not None:
250
- try:
251
- move = self.predict_fn(fen, player)
252
- if move and isinstance(move, str):
253
- return {"move": move}
254
- except Exception:
255
- log.exception("Model prediction failed for FEN=%s", fen)
256
-
257
- # Fallback: random legal move
258
- moves = self._legal_moves(fen)
259
- if not moves:
260
- return {"error": "No legal moves available.", "move": "0000"}
261
-
262
- # Prefer captures (diagonal moves in Breakthrough)
263
- captures = [m for m in moves if m[0] != m[2]]
264
- move = random.choice(captures) if captures else random.choice(moves)
265
- return {"move": move}
266
-
267
- # ──────────────────────────────────────────
268
- # Minimal legal-move generator (self-contained)
269
- # ──────────────────────────────────────────
270
- @staticmethod
271
- def _legal_moves(fen: str) -> list[str]:
272
- """Return all legal UCI moves for the given Breakthrough FEN.
273
-
274
- This is a minimal, self-contained implementation so the
275
- handler does not depend on the server's breakthrough_engine.
276
- """
277
- parts = fen.strip().split()
278
- turn = parts[1] if len(parts) > 1 else WHITE
279
-
280
- # Parse ranks → grid
281
- grid: list[list[str]] = []
282
- for rank_str in parts[0].split("/"):
283
- row: list[str] = []
284
- for ch in rank_str:
285
- if ch.isdigit():
286
- row.extend([EMPTY] * int(ch))
287
- else:
288
- row.append(ch)
289
- grid.append(row)
290
-
291
- piece = WHITE_PIECE if turn == WHITE else BLACK_PIECE
292
- direction = -1 if turn == WHITE else 1
293
- moves: list[str] = []
294
-
295
- for r in range(BOARD_SIZE):
296
- for c in range(BOARD_SIZE):
297
- if grid[r][c] != piece:
298
- continue
299
- nr = r + direction
300
- if nr < 0 or nr >= BOARD_SIZE:
301
- continue
302
- # Straight forward on empty
303
- if grid[nr][c] == EMPTY:
304
- moves.append(
305
- f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - r]}"
306
- f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - nr]}"
307
- )
308
- # Diagonals — capture or empty
309
- for dc in (-1, 1):
310
- nc = c + dc
311
- if nc < 0 or nc >= BOARD_SIZE:
312
- continue
313
- if grid[nr][nc] == piece:
314
- continue # own piece
315
- moves.append(
316
- f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - r]}"
317
- f"{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}"
318
- )
319
-
320
- return moves