test1978 commited on
Commit
eb90a92
·
verified ·
1 Parent(s): 0491ab3

Delete handler.py

Browse files
Files changed (1) hide show
  1. handler.py +0 -419
handler.py DELETED
@@ -1,419 +0,0 @@
1
- """Model handler used as a HuggingFace-style Endpoint inside the Agladiator sandbox.
2
-
3
- This single-file handler attempts to load model helper modules shipped with
4
- the model (e.g. `chess_mcvs.py`, `matrix_model.py`, `abc_model.py`), a
5
- zone database referenced in `config_data.json`, and instantiate a searcher
6
- (prefer `MCVSSearcher` then `UCTSearcher`).
7
-
8
- It exposes `EndpointHandler` with a simple `__call__(data: dict) -> dict`
9
- API returning `{"move": "e2e4"}` or `{"error": ..., "move": "0000"}`
10
- on failure. The implementation is defensive and logs all issues instead of
11
- raising.
12
- """
13
- from __future__ import annotations
14
-
15
- import importlib.util
16
- import json
17
- import logging
18
- import os
19
- import random
20
- import sys
21
- import traceback
22
- from pathlib import Path
23
- from typing import Any, Dict, Optional, Tuple
24
-
25
- import numpy as np
26
- import chess
27
-
28
- log = logging.getLogger(__name__)
29
-
30
-
31
- def _safe_load_json(path: Path) -> Dict[str, Any]:
32
- try:
33
- with open(path, encoding="utf-8") as fh:
34
- return json.load(fh)
35
- except Exception:
36
- log.debug("Could not load JSON %s", path, exc_info=True)
37
- return {}
38
-
39
-
40
- class SimpleGame:
41
- """Minimal game wrapper expected by many searcher implementations.
42
-
43
- It exposes a `.board` attribute (a python-chess Board) and a `copy()`
44
- method returning a shallow copy with its own Board instance.
45
- """
46
-
47
- def __init__(self, board: chess.Board):
48
- self.board = board
49
-
50
- def copy(self):
51
- return SimpleGame(self.board.copy())
52
-
53
-
54
- class EndpointHandler:
55
- """Official endpoint handler used inside sandboxed model runs.
56
-
57
- Usage: `h = EndpointHandler(path="/model"); h({"inputs": {"fen":..., "player":"w"}})`
58
- """
59
-
60
- def __init__(self, path: str):
61
- self.path = Path(path)
62
- self.loaded_modules: Dict[str, Any] = {}
63
- self.main_module = None
64
- self.zone_db = None
65
- self.searcher = None
66
-
67
- # Tunables from environment
68
- self.device = os.environ.get("DEVICE", "cpu")
69
- try:
70
- self.lambda_zone = float(os.environ.get("LAMBDA_ZONE", "0.0"))
71
- except Exception:
72
- self.lambda_zone = 0.0
73
- try:
74
- self.k_zone = int(os.environ.get("K_ZONE", "5"))
75
- except Exception:
76
- self.k_zone = 5
77
- try:
78
- self.dirichlet_alpha = float(os.environ.get("DIRICHLET_ALPHA", "0.3"))
79
- except Exception:
80
- self.dirichlet_alpha = 0.3
81
-
82
- # Make the model path importable for relative imports
83
- try:
84
- if str(self.path) not in sys.path:
85
- sys.path.insert(0, str(self.path))
86
- except Exception:
87
- log.debug("Could not insert model path into sys.path", exc_info=True)
88
-
89
- # Load config files if present
90
- model_config = _safe_load_json(self.path / "config_model.json")
91
- data_config = _safe_load_json(self.path / "config_data.json")
92
-
93
- # Import files listed in config_model.json
94
- files = model_config.get("files") or []
95
- for f in files:
96
- try:
97
- mod_name = Path(f).stem
98
- mod_path = self.path / f
99
- if not mod_path.exists():
100
- log.debug("Listed module %s not found at %s", f, mod_path)
101
- continue
102
- spec = importlib.util.spec_from_file_location(mod_name, str(mod_path))
103
- if spec is None or spec.loader is None:
104
- log.warning("Could not create spec for %s", mod_path)
105
- continue
106
- mod = importlib.util.module_from_spec(spec)
107
- sys.modules[mod_name] = mod
108
- spec.loader.exec_module(mod)
109
- self.loaded_modules[mod_name] = mod
110
- log.info("Loaded module %s", mod_name)
111
- except Exception:
112
- log.exception("Failed to import listed module %s", f)
113
-
114
- # Determine main module
115
- main_name_raw = (model_config.get("main") or "").strip()
116
- if main_name_raw:
117
- main_name = Path(main_name_raw).stem
118
- self.main_module = self.loaded_modules.get(main_name) or None
119
-
120
- # If chess_mcvs.py is present in repo root, import it (preferred source of Chess/MCVSSearcher)
121
- try:
122
- chess_mcvs_path = self.path / "chess_mcvs.py"
123
- if chess_mcvs_path.exists():
124
- spec = importlib.util.spec_from_file_location("chess_mcvs", str(chess_mcvs_path))
125
- if spec and spec.loader:
126
- cm = importlib.util.module_from_spec(spec)
127
- sys.modules["chess_mcvs"] = cm
128
- spec.loader.exec_module(cm)
129
- self.loaded_modules["chess_mcvs"] = cm
130
- if self.main_module is None:
131
- self.main_module = cm
132
- log.info("Imported chess_mcvs from model path")
133
- except Exception:
134
- log.debug("Could not import chess_mcvs.py", exc_info=True)
135
-
136
- # Load zone DB if referenced
137
- zone_filename = (
138
- data_config.get("zone_db")
139
- or data_config.get("zone_db_filename")
140
- or model_config.get("zone_db")
141
- or model_config.get("zone_db_filename")
142
- or None
143
- )
144
-
145
- if zone_filename:
146
- # Try local candidates first
147
- candidates = [self.path / zone_filename, self.path / "data" / zone_filename]
148
- loaded = False
149
- for cand in candidates:
150
- try:
151
- if cand.exists():
152
- self.zone_db = np.load(str(cand), allow_pickle=True)
153
- log.info("Loaded zone DB from %s", cand)
154
- loaded = True
155
- break
156
- except Exception:
157
- log.debug("Failed to load zone DB from %s", cand, exc_info=True)
158
-
159
- # Optional: try to download from HF_DATA_REPO_ID if set and huggingface_hub present
160
- if not loaded:
161
- hf_repo = os.environ.get("HF_DATA_REPO_ID")
162
- if hf_repo:
163
- try:
164
- from huggingface_hub import hf_hub_download
165
-
166
- # Download into model path so we don't write outside repo
167
- try:
168
- dl_path = hf_hub_download(repo_id=hf_repo, filename=zone_filename, cache_dir=str(self.path))
169
- except TypeError:
170
- # Older hf_hub_download signature
171
- dl_path = hf_hub_download(repo_id=hf_repo, filename=zone_filename)
172
- if Path(dl_path).exists():
173
- try:
174
- self.zone_db = np.load(str(dl_path), allow_pickle=True)
175
- loaded = True
176
- log.info("Downloaded and loaded zone DB %s from HF repo %s", zone_filename, hf_repo)
177
- except Exception:
178
- log.debug("Downloaded zone DB but failed to load %s", dl_path, exc_info=True)
179
- except Exception:
180
- log.debug("Could not download zone DB from HF repo (hf_hub missing or download failed)", exc_info=True)
181
-
182
- # Initialise a searcher class if available
183
- self._init_searcher()
184
-
185
- def _find_searcher_class(self):
186
- # Prefer MCVSSearcher in chess_mcvs module, then look through loaded modules
187
- candidates = []
188
- # If chess_mcvs loaded explicitly, check it first
189
- cm = self.loaded_modules.get("chess_mcvs")
190
- if cm is not None:
191
- for name in ("MCVSSearcher", "ChessSearcher", "UCTSearcher"):
192
- if hasattr(cm, name):
193
- return getattr(cm, name), cm
194
-
195
- # Otherwise check main module then all loaded modules
196
- if self.main_module is not None:
197
- for name in ("MCVSSearcher", "ChessSearcher", "UCTSearcher"):
198
- if hasattr(self.main_module, name):
199
- return getattr(self.main_module, name), self.main_module
200
-
201
- for mod_name, mod in self.loaded_modules.items():
202
- for name in ("MCVSSearcher", "ChessSearcher", "UCTSearcher"):
203
- if hasattr(mod, name):
204
- return getattr(mod, name), mod
205
-
206
- # Last resort: try to import a module named 'mcvs' or 'searcher' in path
207
- try:
208
- import importlib
209
-
210
- for candidate in ("mcvs", "searcher", "uct_searcher"):
211
- try:
212
- m = importlib.import_module(candidate)
213
- for name in ("MCVSSearcher", "ChessSearcher", "UCTSearcher"):
214
- if hasattr(m, name):
215
- return getattr(m, name), m
216
- except Exception:
217
- continue
218
- except Exception:
219
- pass
220
-
221
- return None, None
222
-
223
- def _instantiate_searcher(self, searcher_cls):
224
- # Try a series of sensible constructor signatures
225
- kwargs_variants = [
226
- {
227
- "policy_net": None,
228
- "value_net": None,
229
- "zone_db": self.zone_db,
230
- "use_nets": False,
231
- "lambda_zone": float(self.lambda_zone),
232
- "k_zone": int(self.k_zone),
233
- "device": self.device,
234
- "dirichlet_alpha": float(self.dirichlet_alpha),
235
- },
236
- {
237
- "policy_net": None,
238
- "value_net": None,
239
- "zone_db": self.zone_db,
240
- "use_nets": False,
241
- "lambda_zone": float(self.lambda_zone),
242
- "k_zone": int(self.k_zone),
243
- "device": self.device,
244
- },
245
- {"zone_db": self.zone_db, "use_nets": False},
246
- {"zone_db": self.zone_db},
247
- {},
248
- ]
249
-
250
- for kwargs in kwargs_variants:
251
- try:
252
- # Filter out None values which some constructors may not accept
253
- filtered = {k: v for k, v in kwargs.items() if v is not None}
254
- inst = searcher_cls(**filtered)
255
- log.info("Instantiated searcher %s with kwargs %s", getattr(searcher_cls, "__name__", str(searcher_cls)), list(filtered.keys()))
256
- return inst
257
- except TypeError:
258
- # Signature mismatch — try next variant
259
- continue
260
- except Exception:
261
- log.debug("Failed instantiating %s with %s", getattr(searcher_cls, "__name__", str(searcher_cls)), kwargs, exc_info=True)
262
- continue
263
-
264
- log.warning("Could not instantiate searcher class %s; search will be disabled.", getattr(searcher_cls, "__name__", str(searcher_cls)))
265
- return None
266
-
267
- def _init_searcher(self):
268
- try:
269
- searcher_cls, mod = self._find_searcher_class()
270
- if searcher_cls is None:
271
- log.info("No searcher class found in model files.")
272
- self.searcher = None
273
- return
274
- self.searcher = self._instantiate_searcher(searcher_cls)
275
- except Exception:
276
- log.exception("Failed to initialise searcher")
277
- self.searcher = None
278
-
279
- def _extract_move_from_search_result(self, res, board: chess.Board) -> Optional[str]:
280
- """Extract a best UCI move string from a search result.
281
-
282
- Handles tuples (visits, sims), visits dicts, and simple string returns.
283
- """
284
- try:
285
- # Case: tuple (visits, sims)
286
- if isinstance(res, tuple) and len(res) >= 1:
287
- visits = res[0]
288
- if isinstance(visits, dict) and visits:
289
- best = max(visits.items(), key=lambda kv: float(kv[1]))[0]
290
- if hasattr(best, "uci"):
291
- return best.uci()
292
- return str(best)
293
-
294
- # Case: visits dict
295
- if isinstance(res, dict):
296
- # If it looks like a visits dict (values numeric)
297
- if res and all(isinstance(v, (int, float)) for v in res.values()):
298
- best = max(res.items(), key=lambda kv: float(kv[1]))[0]
299
- if hasattr(best, "uci"):
300
- return best.uci()
301
- return str(best)
302
- # Otherwise maybe {"move": "e2e4"}
303
- if "move" in res and isinstance(res["move"], str):
304
- return res["move"].strip()
305
-
306
- # Case: list of candidates
307
- if isinstance(res, list) and res:
308
- first = res[0]
309
- if isinstance(first, dict) and "move" in first:
310
- return first["move"].strip()
311
- if isinstance(first, str):
312
- return first.strip()
313
-
314
- # Case: string
315
- if isinstance(res, str) and res:
316
- return res.strip()
317
-
318
- except Exception:
319
- log.debug("Failed extracting move from search result", exc_info=True)
320
- return None
321
-
322
- def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
323
- """Run a single prediction call.
324
-
325
- Accepts either `{"inputs": {...}}` or a plain dict with `fen`, `player`,
326
- and optional `time_budget`.
327
- """
328
- try:
329
- inputs = data.get("inputs") if isinstance(data, dict) and "inputs" in data else data
330
- if not isinstance(inputs, dict):
331
- return {"error": "invalid_inputs", "move": "0000"}
332
-
333
- fen = inputs.get("fen", "")
334
- player = inputs.get("player", "w")
335
-
336
- # Validate FEN
337
- try:
338
- board = chess.Board(fen)
339
- except Exception as e:
340
- return {"error": "invalid_fen", "message": str(e), "move": "0000"}
341
-
342
- # Resolve time budget
343
- try:
344
- tb = inputs.get("time_budget", 1.0)
345
- tb = float(tb) if tb is not None and tb != "" else 1.0
346
- except Exception:
347
- tb = 1.0
348
- # Default and clamp
349
- if tb <= 0:
350
- tb = 1.0
351
- tb = min(float(tb), 12.0)
352
-
353
- # Build a Game object for the searcher
354
- # Prefer chess_mcvs.Chess() if available (it may provide richer API)
355
- game_obj = None
356
- try:
357
- cm = self.loaded_modules.get("chess_mcvs") or self.loaded_modules.get("chess_mcvs.py")
358
- if cm and hasattr(cm, "Chess"):
359
- try:
360
- game_obj = getattr(cm, "Chess")()
361
- # Set board attribute expected by searchers
362
- game_obj.board = board
363
- except Exception:
364
- game_obj = None
365
- # Fallback to a minimal wrapper
366
- except Exception:
367
- game_obj = None
368
- if game_obj is None:
369
- game_obj = SimpleGame(board)
370
-
371
- # If we have a searcher with search_with_time_budget, run it
372
- if self.searcher is not None and hasattr(self.searcher, "search_with_time_budget"):
373
- try:
374
- # Try calling with game.copy() and tb
375
- try:
376
- result = self.searcher.search_with_time_budget(game_obj.copy(), tb)
377
- except TypeError:
378
- # Try alternative signatures
379
- try:
380
- result = self.searcher.search_with_time_budget(game_obj, tb)
381
- except TypeError:
382
- try:
383
- result = self.searcher.search_with_time_budget(tb)
384
- except Exception:
385
- result = None
386
-
387
- # Attempt to extract a move
388
- mv = self._extract_move_from_search_result(result, board)
389
- if mv:
390
- # Verify legality
391
- try:
392
- candidate = chess.Move.from_uci(mv)
393
- if candidate in board.legal_moves:
394
- return {"move": mv}
395
- except Exception:
396
- # Not a valid UCI or illegal on this board
397
- log.debug("Searcher returned invalid or illegal move %s", mv)
398
-
399
- except Exception:
400
- log.exception("Searcher raised an exception during search_with_time_budget")
401
-
402
- # Final fallback: choose a legal move locally (prefer captures)
403
- try:
404
- legal = list(board.legal_moves)
405
- if not legal:
406
- return {"error": "no_legal_moves", "move": "0000"}
407
- captures = [m for m in legal if board.is_capture(m)]
408
- if captures:
409
- pick = random.choice(captures)
410
- else:
411
- pick = random.choice(legal)
412
- return {"move": pick.uci()}
413
- except Exception:
414
- log.exception("Fallback legal-move selection failed")
415
- return {"error": "fallback_failed", "move": "0000"}
416
-
417
- except Exception:
418
- log.exception("Unhandled exception in EndpointHandler.__call__")
419
- return {"error": "internal_error", "move": "0000"}