caro5 / app.py
Pedro de Carvalho
Update app
fce1240
Raw
History Blame Contribute Delete
4.33 kB
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(
"""
<main style="font-family: system-ui, sans-serif; max-width: 720px; margin: 48px auto; line-height: 1.5">
<h1>Caro5 build missing</h1>
<p>The React bundle was not found at <code>client/dist</code>.</p>
<p>Copy or build the web bundle before launching this Space.</p>
</main>
""",
status_code=503,
)
if __name__ == "__main__":
app.launch(server_name="0.0.0.0", server_port=7860)