Spaces:
Sleeping
Sleeping
| """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 | |
| def get_config(): | |
| canary_info = agent.get_canary_info() | |
| return {"status": "ok", "profile": canary_info["current_profile"], "canary_ratio": canary_info["canary_ratio"]} | |
| 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}, | |
| } | |
| def get_requests(): | |
| history = agent.get_req_resp_history() | |
| return {"success": True, "total": len(history), "history": history} | |
| 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""" | |
| <div class='req-item'> | |
| <div class='header'> | |
| <span class='type'>{html_lib.escape(str(req_type))}</span> | |
| <span class='meta'>{html_lib.escape(str(status))} | Round {html_lib.escape(str(round_num))} | Action: {html_lib.escape(str(action))}</span> | |
| </div> | |
| <details><summary>Request</summary><pre>{html_lib.escape(str(req_data))}</pre></details> | |
| <details><summary>Response</summary><pre>{html_lib.escape(str(resp_data))}</pre></details> | |
| </div>""" | |
| return HTMLResponse(content=f""" | |
| <!DOCTYPE html><html><head><meta charset='UTF-8'><title>Request History</title> | |
| <style>body{{font-family:monospace;margin:20px;background:#f5f5f5}} | |
| .req-item{{background:white;margin:10px 0;padding:15px;border-radius:5px}} | |
| .header{{display:flex;justify-content:space-between;margin-bottom:10px}} | |
| .type{{font-weight:bold}} .meta{{color:#666;font-size:0.9em}} | |
| details{{margin:5px 0}} summary{{cursor:pointer;color:#0066cc;font-weight:bold}} | |
| pre{{background:#f0f0f0;padding:10px;overflow-x:auto;white-space:pre-wrap;font-size:0.85em}}</style> | |
| </head><body><h2>Request History ({len(history)})</h2>{html_items}</body></html>""") | |
| def clear_requests(): | |
| agent.clear_req_resp_history() | |
| return {"success": True} | |
| 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() | |
| }, | |
| } | |
| 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)}) | |
| 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)}) | |
| 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) | |