from __future__ import annotations from pathlib import Path from typing import Any, Literal from fastapi import HTTPException from fastapi.responses import FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from gradio import Server from pydantic import BaseModel, ConfigDict, Field from caro5_bot import BotRequest, select_move from caro5_bot.tactical import BotError, status as bot_status ROOT = Path(__file__).resolve().parent DIST_DIR = ROOT / "client" / "dist" app = Server() class BotMoveRequest(BaseModel): model_config = ConfigDict(populate_by_name=True) board: dict[str, dict[str, Any]] = Field(default_factory=dict) current_player: int = Field(alias="currentPlayer") player_symbols: tuple[str, str] = Field(default=("X", "O"), alias="playerSymbols") rules: dict[str, Any] = Field(default_factory=dict) board_size: int | None = Field(default=None, alias="boardSize") profile: Literal["normal", "expert", "ai", "balanced"] = "normal" move_number: int | None = Field(default=None, alias="moveNumber") seed: int | None = None @app.api(name="health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/api/bot/status") async def api_bot_status() -> dict[str, Any]: status = bot_status() return { "available": status.available, "engine": status.engine, "profiles": list(status.profiles), "defaultProfile": status.default_profile, "modelAvailable": status.model_available, "modelPath": status.model_path, "metadataPath": status.metadata_path, "modelError": status.model_error, "metadata": status.metadata, } @app.post("/api/bot/move") async def api_bot_move(request: BotMoveRequest) -> dict[str, Any]: try: response = select_move( BotRequest( board=request.board, current_player=request.current_player, player_symbols=request.player_symbols, rules=request.rules, board_size=request.board_size, profile=request.profile, move_number=request.move_number, seed=request.seed, ) ) except BotError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return { "x": response.x, "y": response.y, "move": {"x": response.x, "y": response.y}, "source": response.source, "profile": response.profile, "elapsedMs": response.elapsed_ms, "seed": response.seed, "legal": response.legal, "modelAvailable": response.model_available, } def static_file(name: str) -> FileResponse: path = DIST_DIR / name if path.exists() and path.is_file(): return FileResponse(path) raise HTTPException(status_code=404) for route_name in ("assets", "sounds"): route_dir = DIST_DIR / route_name if route_dir.exists(): app.mount(f"/{route_name}", StaticFiles(directory=route_dir), name=route_name) @app.get("/favicon.svg") async def favicon(): return static_file("favicon.svg") @app.get("/icons.svg") async def icons(): return static_file("icons.svg") @app.get("/vite.svg") async def vite_favicon_alias(): return static_file("favicon.svg") @app.get("/") async def root(): if (DIST_DIR / "index.html").exists(): return FileResponse(DIST_DIR / "index.html") return missing_build_response() @app.get("/{path:path}", response_class=HTMLResponse) async def react_app(path: str): index = DIST_DIR / "index.html" if index.exists(): return FileResponse(index) return missing_build_response() def missing_build_response() -> HTMLResponse: return HTMLResponse( """

Caro5 build missing

The React bundle was not found at client/dist.

Copy or build the web bundle before launching this Space.

""", status_code=503, ) if __name__ == "__main__": app.launch(server_name="0.0.0.0", server_port=7860)