"""FastAPI entry point — new five-layer architecture. 来源:REFACTOR_HANDBOOK §2.2 (app.py, L5) + §2.3 (数据流) Keeps the same HTTP interface: /agent/perceive, /agent/interact, /debug/* """ from __future__ import annotations import html as html_lib import logging import sys from agent_build_sdk.server.server import EndpointServer from fastapi.responses import HTMLResponse, JSONResponse, Response from werewolftown.agent import WerewolfTownAgent from werewolftown.state.persistence import ( DEFAULT_REPLAY_DIR, game_summaries, read_replay, zip_all_replays, ) from werewolftown.transport.replay_uploader import is_enabled as replay_upload_enabled from werewolftown.transport.replay_uploader import start_replay_scheduler logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) def register_debug_endpoints(server: EndpointServer) -> None: """Register /debug/* endpoints for observability.""" agent = server.agent @server.app.get("/config") def get_config(): canary_info = agent.get_canary_info() return {"status": "ok", "profile": canary_info["current_profile"], "canary_ratio": canary_info["canary_ratio"]} @server.app.get("/health") def health_check(): """Health check endpoint for monitoring.""" canary_info = agent.get_canary_info() return { "status": "ok", "canary_ratio": canary_info["canary_ratio"], "current_profile": canary_info["current_profile"], "current_game_id": canary_info["current_game_id"], "tests": 441, "coverage": {"strategies": 92, "transport": 93}, } @server.app.get("/debug/requests") def get_requests(): history = agent.get_req_resp_history() return {"success": True, "total": len(history), "history": history} @server.app.get("/debug/requests/view") def view_requests(): history = agent.get_req_resp_history() html_items = "" for item in reversed(history): req_data = item.get("req", {}) resp_data = item.get("resp", {}) req_type = item.get("type", "?") status = req_data.get("status", "?") round_num = req_data.get("round", "?") action = resp_data.get("action", "-") if resp_data else "-" html_items += f"""
{html_lib.escape(str(req_type))} {html_lib.escape(str(status))} | Round {html_lib.escape(str(round_num))} | Action: {html_lib.escape(str(action))}
Request
{html_lib.escape(str(req_data))}
Response
{html_lib.escape(str(resp_data))}
""" return HTMLResponse(content=f""" Request History

Request History ({len(history)})

{html_items}""") @server.app.post("/debug/requests/clear") def clear_requests(): agent.clear_req_resp_history() return {"success": True} @server.app.get("/debug/state") def get_state(): """Debug endpoint: current game state.""" state = agent.get_state() return { "player_id": state.player_id, "round": state.round, "cash": state.cash, "plots": sorted(state.plots), "shops": list(state.shops), "built_on": {str(k): v for k, v in state.built_on.items()}, "other_players": { pid: {"cash": pv.cash, "plots": sorted(pv.plots), "shops": list(pv.shops)} for pid, pv in state.other_players.items() }, } @server.app.get("/debug/games") def list_saved_games(): """List all saved game replays from persistent storage.""" try: games = game_summaries() return { "success": True, "base_dir": DEFAULT_REPLAY_DIR, "upload_enabled": replay_upload_enabled(), "total": len(games), "games": games, } except Exception as exc: # noqa: BLE001 -- debug endpoint must never 500 the app logger.warning(f"[/debug/games] failed: {exc}") return JSONResponse(status_code=500, content={"success": False, "error": str(exc)}) @server.app.get("/debug/replay/{game_id}") def get_replay(game_id: str): """Return one game's full replay (events + decisions + meta) as JSON.""" try: return {"success": True, **read_replay(game_id)} except Exception as exc: # noqa: BLE001 -- debug endpoint must never 500 the app logger.warning(f"[/debug/replay] failed for {game_id}: {exc}") return JSONResponse(status_code=500, content={"success": False, "error": str(exc)}) @server.app.get("/debug/replays.zip") def download_all_replays(): """Download every saved replay as a single zip archive.""" try: data = zip_all_replays() headers = {"Content-Disposition": 'attachment; filename="replays.zip"'} return Response(content=data, media_type="application/zip", headers=headers) except Exception as exc: # noqa: BLE001 -- debug endpoint must never 500 the app logger.warning(f"[/debug/replays.zip] failed: {exc}") return JSONResponse(status_code=500, content={"success": False, "error": str(exc)}) if __name__ == "__main__": try: logger.info("Starting WerewolfTown agent (five-layer architecture)") agent = WerewolfTownAgent(player_id="") server = EndpointServer(agent) register_debug_endpoints(server) # Keep a module-level reference so the background uploader is not GC'd. _replay_scheduler = start_replay_scheduler() logger.info("Server starting on port 7860") server.start() except Exception as e: logger.error(f"STARTUP CRASH: {e}", exc_info=True) sys.exit(1)