Download handler.py from test1978/breakthrough-model: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/test1978/breakthrough-model/resolve/9940d2a4e827b7a7232d4dae736bd6b7cb7fcdef/handler.py
- Command line
-
hf download hf://test1978/breakthrough-model@9940d2a4e827b7a7232d4dae736bd6b7cb7fcdef/handler.py
-
curl -L -o handler.py https://huggingface.co/test1978/breakthrough-model/resolve/9940d2a4e827b7a7232d4dae736bd6b7cb7fcdef/handler.py
13.3 kB
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # handler.py | |
| # | |
| # Custom handler for HF Inference Endpoints. | |
| # | |
| # When HF Inference Endpoints detects a handler.py | |
| # in the repo root, it uses this class to serve | |
| # predictions instead of the default Transformers | |
| # pipeline. | |
| # | |
| # Expected repo layout: | |
| # | |
| # your-username/breakthrough-model/ | |
| # βββ handler.py β this file | |
| # βββ config_model.json β lists your Python modules + main | |
| # βββ config_data.json β lists data files + zone_db key | |
| # βββ breakthrough_mcvs.py β your UCT search / eval module | |
| # βββ requirements.txt β runtime deps (numpy, huggingface_hub) | |
| # | |
| # config_model.json example: | |
| # | |
| # { | |
| # "files": ["matrix_model.py", "abc_model.py", "breakthrough_mcvs.py"], | |
| # "main": "breakthrough_mcvs" | |
| # } | |
| # | |
| # config_data.json example: | |
| # | |
| # { | |
| # "files": ["breakthrough_zone_db.npz"], | |
| # "zone_db": "breakthrough_zone_db.npz" | |
| # } | |
| # | |
| # Set the HF_DATA_REPO_ID environment variable to | |
| # the HF dataset repo holding your data files. | |
| # | |
| # Request format (sent by Agladiator): | |
| # | |
| # POST / | |
| # { | |
| # "inputs": { | |
| # "fen": "BBBBBBBB/BBBBBBBB/8/8/8/8/WWWWWWWW/WWWWWWWW w", | |
| # "player": "w", | |
| # "game_type": "breakthrough" | |
| # } | |
| # } | |
| # | |
| # Response format (what you must return): | |
| # | |
| # {"move": "a2a3"} | |
| # | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| from __future__ import annotations | |
| import importlib.util | |
| import json | |
| import logging | |
| import os | |
| import random | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| log = logging.getLogger(__name__) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Breakthrough board constants (must match the | |
| # server's breakthrough_engine.py) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| BOARD_SIZE = 8 | |
| _FILES = "abcdefgh" | |
| _RANKS = "12345678" | |
| WHITE, BLACK = "w", "b" | |
| WHITE_PIECE, BLACK_PIECE, EMPTY = "W", "B", "." | |
| class EndpointHandler: | |
| """Custom handler for Breakthrough AI on HF Inference Endpoints. | |
| Lifecycle: | |
| 1. ``__init__`` is called once when the endpoint container starts. | |
| Use it to load your model artifacts (modules, zone DB, etc.). | |
| 2. ``__call__`` is called for every inference request. | |
| It receives the parsed JSON body and must return a dict. | |
| """ | |
| def __init__(self, path: str): | |
| """Load model artefacts from *path* (the cloned repo directory). | |
| Steps: | |
| 1. Read ``config_model.json`` to discover module files and main module. | |
| 2. Dynamically import each listed module. | |
| 3. Read ``config_data.json``, download data files, load zone DB. | |
| 4. Initialise the search engine. | |
| """ | |
| self.path = Path(path) | |
| self.predict_fn = None | |
| self.zone_db = None | |
| self.module = None | |
| # ββ 1. Read config_model.json βββββββββββ | |
| model_config_path = self.path / "config_model.json" | |
| if not model_config_path.exists(): | |
| log.warning("No config_model.json found in %s", path) | |
| model_config: dict = {} | |
| else: | |
| with open(model_config_path) as fh: | |
| model_config = json.load(fh) | |
| main_module_name = model_config.get("main", "") | |
| # ββ 2. Import listed modules ββββββββββββ | |
| # Add repo dir to sys.path so relative imports work. | |
| if str(self.path) not in sys.path: | |
| sys.path.insert(0, str(self.path)) | |
| for mod_file in model_config.get("files", []): | |
| mod_name = mod_file.removesuffix(".py") | |
| mod_path = self.path / mod_file | |
| if not mod_path.exists(): | |
| log.warning("Module file %s not found β skipping.", mod_file) | |
| continue | |
| spec = importlib.util.spec_from_file_location(mod_name, str(mod_path)) | |
| mod = importlib.util.module_from_spec(spec) | |
| sys.modules[mod_name] = mod | |
| spec.loader.exec_module(mod) | |
| log.info("Loaded module: %s", mod_name) | |
| # Mark the module matching "main" as the primary module. | |
| if mod_name == main_module_name: | |
| self.module = mod | |
| # ββ 3. Read config_data.json & load zone DB ββ | |
| data_config_path = self.path / "config_data.json" | |
| if not data_config_path.exists(): | |
| log.warning("No config_data.json found in %s", path) | |
| data_config: dict = {} | |
| else: | |
| with open(data_config_path) as fh: | |
| data_config = json.load(fh) | |
| data_repo = os.environ.get("HF_DATA_REPO_ID", "") | |
| downloaded_files: dict[str, str] = {} | |
| if data_repo: | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| except Exception: | |
| hf_hub_download = None | |
| log.warning( | |
| "huggingface_hub not installed β cannot download data files. Add it to requirements.txt" | |
| ) | |
| if hf_hub_download is not None: | |
| for data_file in data_config.get("files", []): | |
| try: | |
| local_path = hf_hub_download( | |
| repo_id=data_repo, | |
| filename=data_file, | |
| repo_type="dataset", | |
| ) | |
| downloaded_files[data_file] = local_path | |
| log.info("Downloaded %s from %s", data_file, data_repo) | |
| except Exception: | |
| log.warning( | |
| "Failed to download %s from %s β skipping.", | |
| data_file, | |
| data_repo, | |
| ) | |
| else: | |
| log.warning( | |
| "HF_DATA_REPO_ID not set β skipping data file downloads." | |
| ) | |
| zone_db_filename = data_config.get("zone_db", "") | |
| if zone_db_filename and zone_db_filename in downloaded_files: | |
| self.zone_db = np.load( | |
| downloaded_files[zone_db_filename], allow_pickle=True | |
| ) | |
| log.info("Zone DB loaded from data repo %s", data_repo) | |
| elif zone_db_filename: | |
| local_zone = self.path / zone_db_filename | |
| if local_zone.exists(): | |
| self.zone_db = np.load(str(local_zone), allow_pickle=True) | |
| log.info("Zone DB loaded from local file %s", local_zone) | |
| else: | |
| log.warning("No zone DB found for '%s'.", zone_db_filename) | |
| # ββ 4. Initialise the predictor βββββββββ | |
| self._init_predictor() | |
| log.info("EndpointHandler initialised successfully.") | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| # Predictor initialisation | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| def _init_predictor(self): | |
| """Discover and configure the prediction function from the | |
| loaded module. Tries several common patterns: | |
| 1. ``module.get_move(fen, player, zone_db=...)`` | |
| 2. ``module.UCTSearcher(zone_db=...).search(fen, player)`` | |
| 3. ``module.predict(fen, player)`` | |
| """ | |
| if self.module is None: | |
| log.warning("No module loaded β predictions will use random fallback.") | |
| return | |
| # Pattern 1: module exposes a top-level get_move() | |
| if hasattr(self.module, "get_move"): | |
| raw_fn = self.module.get_move | |
| self.predict_fn = lambda fen, player: raw_fn( | |
| fen, player, zone_db=self.zone_db | |
| ) | |
| log.info("Using module.get_move() for predictions.") | |
| return | |
| # Pattern 2: module exposes a UCTSearcher class | |
| if hasattr(self.module, "UCTSearcher"): | |
| searcher_cls = self.module.UCTSearcher | |
| kwargs = {} | |
| if self.zone_db is not None: | |
| kwargs["zone_db"] = self.zone_db | |
| searcher = searcher_cls(**kwargs) | |
| self.predict_fn = lambda fen, player: searcher.search(fen, player) | |
| log.info("Using module.UCTSearcher for predictions.") | |
| return | |
| # Pattern 3: module exposes a predict() | |
| if hasattr(self.module, "predict"): | |
| raw_fn = self.module.predict | |
| self.predict_fn = lambda fen, player: raw_fn(fen, player) | |
| log.info("Using module.predict() for predictions.") | |
| return | |
| log.warning( | |
| "Module %s has no get_move / UCTSearcher / predict β " | |
| "predictions will use random fallback.", | |
| self.module.__name__, | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| # Inference entry point | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| def __call__(self, data: dict) -> dict: | |
| """Handle an inference request. | |
| Expected *data*:: | |
| {"inputs": {"fen": "...", "player": "w"}} | |
| Returns:: | |
| {"move": "a2a3"} | |
| Falls back to a random legal move if the model fails. | |
| """ | |
| inputs = data.get("inputs", data) | |
| fen = inputs.get("fen", "") | |
| player = inputs.get("player", WHITE) | |
| if not fen: | |
| return {"error": "Missing 'fen' in request."} | |
| # Try model prediction | |
| if self.predict_fn is not None: | |
| try: | |
| move = self.predict_fn(fen, player) | |
| if move and isinstance(move, str): | |
| return {"move": move} | |
| except Exception: | |
| log.exception("Model prediction failed for FEN=%s", fen) | |
| # Fallback: random legal move | |
| moves = self._legal_moves(fen) | |
| if not moves: | |
| return {"error": "No legal moves available.", "move": "0000"} | |
| # Prefer captures (diagonal moves in Breakthrough) | |
| captures = [m for m in moves if m[0] != m[2]] | |
| move = random.choice(captures) if captures else random.choice(moves) | |
| return {"move": move} | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| # Minimal legal-move generator (self-contained) | |
| # ββββββββββββββββββββββββββββββββββββββββββ | |
| def _legal_moves(fen: str) -> list[str]: | |
| """Return all legal UCI moves for the given Breakthrough FEN. | |
| This is a minimal, self-contained implementation so the | |
| handler does not depend on the server's breakthrough_engine. | |
| """ | |
| parts = fen.strip().split() | |
| turn = parts[1] if len(parts) > 1 else WHITE | |
| # Parse ranks β grid | |
| grid: list[list[str]] = [] | |
| for rank_str in parts[0].split("/"): | |
| row: list[str] = [] | |
| for ch in rank_str: | |
| if ch.isdigit(): | |
| row.extend([EMPTY] * int(ch)) | |
| else: | |
| row.append(ch) | |
| grid.append(row) | |
| piece = WHITE_PIECE if turn == WHITE else BLACK_PIECE | |
| direction = -1 if turn == WHITE else 1 | |
| moves: list[str] = [] | |
| for r in range(BOARD_SIZE): | |
| for c in range(BOARD_SIZE): | |
| if grid[r][c] != piece: | |
| continue | |
| nr = r + direction | |
| if nr < 0 or nr >= BOARD_SIZE: | |
| continue | |
| # Straight forward on empty | |
| if grid[nr][c] == EMPTY: | |
| moves.append( | |
| f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - r]}" | |
| f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - nr]}" | |
| ) | |
| # Diagonals β capture or empty | |
| for dc in (-1, 1): | |
| nc = c + dc | |
| if nc < 0 or nc >= BOARD_SIZE: | |
| continue | |
| if grid[nr][nc] == piece: | |
| continue # own piece | |
| moves.append( | |
| f"{_FILES[c]}{_RANKS[BOARD_SIZE - 1 - r]}" | |
| f"{_FILES[nc]}{_RANKS[BOARD_SIZE - 1 - nr]}" | |
| ) | |
| return moves | |