main.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. #!/usr/bin/env python3
  2. """
  3. GuessWhoAmI Game - FastAPI backend main file
  4. Provides RESTful API for frontend
  5. """
  6. import uuid
  7. import logging
  8. from typing import Dict
  9. from fastapi import FastAPI, HTTPException, status
  10. from fastapi.middleware.cors import CORSMiddleware
  11. from game_logic import GameSession
  12. from agents import HistoricalFigureAgent
  13. from config import get_config
  14. from models import (
  15. ChatRequest, GuessRequest, StartRequest,
  16. HintRequest, EndRequest, GameResponse,
  17. )
  18. # Initialize config
  19. config = get_config()
  20. # Configure logging
  21. import os as _os
  22. _LOG_PATH = _os.path.normpath(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "logs", "backend.log"))
  23. _os.makedirs(_os.path.dirname(_LOG_PATH), exist_ok=True)
  24. _log_formatter = logging.Formatter(
  25. fmt="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
  26. datefmt="%Y-%m-%d %H:%M:%S",
  27. )
  28. # File handler — Python owns the fd, so truncation is safe
  29. _file_handler = logging.FileHandler(_LOG_PATH, mode="a", encoding="utf-8")
  30. _file_handler.setFormatter(_log_formatter)
  31. # Use addHandler directly instead of basicConfig (basicConfig is a no-op if root logger
  32. # already has handlers, e.g. when uvicorn pre-configures logging before our code runs)
  33. _root_logger = logging.getLogger()
  34. _root_logger.setLevel(logging.INFO)
  35. _root_logger.addHandler(_file_handler)
  36. logger = logging.getLogger("game.main")
  37. def _clear_log_file() -> None:
  38. """Clear the log file by truncating it and reopening our own FileHandler."""
  39. # Only operate on our own _file_handler, leave uvicorn/other handlers untouched
  40. _file_handler.acquire()
  41. try:
  42. if _file_handler.stream is not None:
  43. _file_handler.stream.close()
  44. _file_handler.stream = None
  45. finally:
  46. _file_handler.release()
  47. # Truncate the file
  48. with open(_LOG_PATH, "w", encoding="utf-8") as f:
  49. pass
  50. # Reopen our handler in append mode
  51. _file_handler.acquire()
  52. try:
  53. _file_handler.stream = open(_LOG_PATH, "a", encoding="utf-8")
  54. finally:
  55. _file_handler.release()
  56. # Create FastAPI app
  57. app = FastAPI(
  58. title="猜猜我是谁游戏API",
  59. description="基于hello_agents框架的GuessWhoAmI游戏后端API",
  60. version="1.0.0"
  61. )
  62. # Configure CORS
  63. app.add_middleware(
  64. CORSMiddleware,
  65. allow_origins=["*"],
  66. allow_credentials=True,
  67. allow_methods=["*"],
  68. allow_headers=["*"],
  69. )
  70. # Global session storage: session_id -> (GameSession, HistoricalFigureAgent)
  71. active_sessions: Dict[str, tuple] = {}
  72. # Helper functions
  73. def get_session_pair(session_id: str):
  74. """Get game session and agent, raise exception if not found"""
  75. if session_id not in active_sessions:
  76. raise HTTPException(
  77. status_code=status.HTTP_404_NOT_FOUND,
  78. detail="会话不存在或已过期"
  79. )
  80. return active_sessions[session_id]
  81. def create_response(success: bool, message: str, data: dict = None, error: str = None) -> GameResponse:
  82. """Create standardized response"""
  83. return GameResponse(
  84. success=success,
  85. message=message,
  86. data=data,
  87. error=error
  88. )
  89. # API endpoints
  90. @app.get("/")
  91. async def root():
  92. """Root endpoint"""
  93. return {
  94. "message": "猜猜我是谁游戏API",
  95. "version": "1.0.0",
  96. "docs": "/docs"
  97. }
  98. @app.post("/api/game/start", response_model=GameResponse)
  99. async def start_game(request: StartRequest):
  100. """Start a new game"""
  101. try:
  102. # Clear log file via FileHandler to avoid NUL bytes
  103. _clear_log_file()
  104. logger.info("[START] Log file cleared")
  105. session_id = str(uuid.uuid4())
  106. # GameSession auto-initializes and picks a random figure
  107. game_session = GameSession()
  108. # Create Agent with the game session
  109. agent = HistoricalFigureAgent(game_session)
  110. # Store session and agent together
  111. active_sessions[session_id] = (game_session, agent)
  112. figure_name = game_session.current_figure.get("name", "未知")
  113. logger.info(f"[START] session_id={session_id} | figure={figure_name} | max_questions={game_session.max_questions} | max_hints={game_session.max_hints}")
  114. welcome_message = (
  115. f"游戏开始!我是一个知名人物,请通过提问来猜测我是谁。\n"
  116. f"你最多可以提问 {game_session.max_questions} 次,使用 {game_session.max_hints} 次提示。\n"
  117. f"开始吧!"
  118. )
  119. return create_response(
  120. success=True,
  121. message="游戏开始成功",
  122. data={
  123. "session_id": session_id,
  124. "welcome_message": welcome_message,
  125. "max_questions": game_session.max_questions,
  126. "max_hints": game_session.max_hints
  127. }
  128. )
  129. except Exception as e:
  130. logger.error(f"[START] 游戏启动失败: {e}", exc_info=True)
  131. return create_response(
  132. success=False,
  133. message="游戏启动失败",
  134. error=str(e)
  135. )
  136. @app.post("/api/game/chat", response_model=GameResponse)
  137. async def chat_with_agent(request: ChatRequest):
  138. """Chat with Agent"""
  139. try:
  140. game_session, agent = get_session_pair(request.session_id)
  141. # Check game state
  142. if game_session.is_game_over:
  143. logger.warning(f"[CHAT] session_id={request.session_id} | 游戏已结束,拒绝消息")
  144. return create_response(
  145. success=False,
  146. message="游戏已结束",
  147. error="请开始新游戏"
  148. )
  149. logger.info(f"[CHAT] session_id={request.session_id} | questions_asked={game_session.questions_asked} | user={request.message!r}")
  150. # Process message via agent
  151. response_message = agent.chat(request.message)
  152. logger.info(f"[CHAT] session_id={request.session_id} | remaining={game_session.max_questions - game_session.questions_asked} | agent={response_message!r}")
  153. return create_response(
  154. success=True,
  155. message="消息处理成功",
  156. data={
  157. "response": response_message,
  158. "remaining_questions": game_session.max_questions - game_session.questions_asked,
  159. "used_hints": game_session.hints_used,
  160. "is_game_over": game_session.is_game_over
  161. }
  162. )
  163. except HTTPException:
  164. raise
  165. except Exception as e:
  166. logger.error(f"[CHAT] session_id={request.session_id} | 消息处理失败: {e}", exc_info=True)
  167. return create_response(
  168. success=False,
  169. message="消息处理失败",
  170. error=str(e)
  171. )
  172. @app.post("/api/game/guess", response_model=GameResponse)
  173. async def guess_figure(request: GuessRequest):
  174. """Guess the historical figure"""
  175. try:
  176. game_session, agent = get_session_pair(request.session_id)
  177. # Check game state
  178. if game_session.is_game_over:
  179. logger.warning(f"[GUESS] session_id={request.session_id} | 游戏已结束,拒绝猜测")
  180. return create_response(
  181. success=False,
  182. message="游戏已结束",
  183. error="请开始新游戏"
  184. )
  185. actual_name = game_session.current_figure.get("name", "未知")
  186. logger.info(f"[GUESS] session_id={request.session_id} | guess={request.guess!r} | actual={actual_name!r}")
  187. # Make guess (agent handles semantic matching via its LLM)
  188. result = agent.make_guess(request.guess)
  189. if result["correct"]:
  190. logger.info(f"[GUESS] session_id={request.session_id} | 猜测正确!figure={actual_name}")
  191. else:
  192. logger.info(f"[GUESS] session_id={request.session_id} | 猜测错误 | is_game_over={game_session.is_game_over}")
  193. return create_response(
  194. success=True,
  195. message="猜测完成",
  196. data={
  197. "is_correct": result["correct"],
  198. "message": result["message"],
  199. "remaining_questions": game_session.max_questions - game_session.questions_asked,
  200. "is_game_over": game_session.is_game_over,
  201. "figure_info": result.get("figure_info"),
  202. "portrait_images": result.get("portrait_images", []),
  203. }
  204. )
  205. except HTTPException:
  206. raise
  207. except Exception as e:
  208. logger.error(f"[GUESS] session_id={request.session_id} | 猜测失败: {e}", exc_info=True)
  209. return create_response(
  210. success=False,
  211. message="猜测失败",
  212. error=str(e)
  213. )
  214. @app.post("/api/game/hint", response_model=GameResponse)
  215. async def get_hint(request: HintRequest):
  216. """Get a hint"""
  217. try:
  218. game_session, agent = get_session_pair(request.session_id)
  219. # Check game state
  220. if game_session.is_game_over:
  221. logger.warning(f"[HINT] session_id={request.session_id} | 游戏已结束,拒绝提示")
  222. return create_response(
  223. success=False,
  224. message="游戏已结束",
  225. error="请开始新游戏"
  226. )
  227. # Get hint
  228. hint_info = game_session.get_hint()
  229. if hint_info.get("available"):
  230. logger.info(f"[HINT] session_id={request.session_id} | level={hint_info.get('hint_level')} | hint={hint_info.get('hint')!r} | remaining={hint_info.get('remaining_hints')}")
  231. else:
  232. logger.info(f"[HINT] session_id={request.session_id} | 提示次数已用完")
  233. return create_response(
  234. success=True,
  235. message="提示获取成功",
  236. data=hint_info
  237. )
  238. except HTTPException:
  239. raise
  240. except Exception as e:
  241. logger.error(f"[HINT] session_id={request.session_id} | 获取提示失败: {e}", exc_info=True)
  242. return create_response(
  243. success=False,
  244. message="获取提示失败",
  245. error=str(e)
  246. )
  247. @app.post("/api/game/end", response_model=GameResponse)
  248. async def end_game(request: EndRequest):
  249. """End the game"""
  250. try:
  251. game_session, agent = get_session_pair(request.session_id)
  252. status_info = game_session.get_game_status()
  253. figure_name = game_session.current_figure.get("name", "未知")
  254. status_info["figure_name"] = figure_name
  255. logger.info(f"[END] session_id={request.session_id} | figure={figure_name} | is_correct={game_session.is_correct} | questions_asked={game_session.questions_asked} | hints_used={game_session.hints_used}")
  256. # Remove from active sessions
  257. del active_sessions[request.session_id]
  258. return create_response(
  259. success=True,
  260. message="游戏结束",
  261. data=status_info
  262. )
  263. except HTTPException:
  264. raise
  265. except Exception as e:
  266. logger.error(f"[END] session_id={request.session_id} | 结束游戏失败: {e}", exc_info=True)
  267. return create_response(
  268. success=False,
  269. message="结束游戏失败",
  270. error=str(e)
  271. )
  272. @app.get("/api/game/status/{session_id}", response_model=GameResponse)
  273. async def get_game_status(session_id: str):
  274. """Get game status"""
  275. try:
  276. game_session, agent = get_session_pair(session_id)
  277. return create_response(
  278. success=True,
  279. message="状态获取成功",
  280. data=game_session.get_game_status()
  281. )
  282. except HTTPException:
  283. raise
  284. except Exception as e:
  285. return create_response(
  286. success=False,
  287. message="状态获取失败",
  288. error=str(e)
  289. )
  290. if __name__ == "__main__":
  291. import uvicorn
  292. uvicorn.run(app, host="0.0.0.0", port=8000)