rui3000 commited on
Commit
1a64ad7
ยท
verified ยท
1 Parent(s): d181154

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -145
app.py CHANGED
@@ -2,7 +2,6 @@ import gradio as gr
2
  import uuid
3
  import httpx
4
  import os
5
- from contextlib import asynccontextmanager
6
 
7
  # FastAPI imports
8
  from fastapi import FastAPI, Request, Body
@@ -27,204 +26,280 @@ class GameResult(Enum):
27
  # --- Gradio Interface ---
28
  class RockPaperScissorsUI:
29
  def __init__(self, game_service):
30
- self.game_service = game_service #
31
- self.session_id = None #
32
- self.ai_models = list(self.game_service.ai_models.keys()) #
33
  self.ai_descriptions = {
34
- "random": "Random AI: Makes completely random moves.", #
35
- "adaptive_markov": "Adaptive Markov AI: Uses entropy-weighted Markov and frequency models to predict your next move." #
36
  }
37
- self.last_move = None #
38
 
39
  async def reset_session(self, session_id: str):
40
- # Use 127.0.0.1 which is the correct loopback address inside the container
41
- async with httpx.AsyncClient(base_url="http://127.0.0.1:7860") as client: #
42
- try:
43
- response = await client.post(
44
- "/api/game/end", # Path is relative to base_url
45
- json={"session_id": session_id}
46
- ) #
47
- print("[DEBUG] /api/game/end response:", response.text) #
48
- except httpx.ConnectError as e:
49
- print(f"[ERROR] Connection to API endpoint failed: {e}")
50
- return {"status": "error", "message": "API connection failed"}
51
- await self.game_service.clear_session(session_id) #
52
- return {"status": "ok"} #
53
 
54
  def create_interface(self):
55
- with gr.Blocks(theme=gr.themes.Soft(), title="Rock Paper Scissors ๐ŸŽฎ") as demo: #
56
- gr.Markdown("# ๐Ÿชจ๐Ÿ“„โœ‚๏ธ Rock Paper Scissors") #
57
 
58
- with gr.Row(): #
59
- with gr.Column(scale=1): #
60
- gr.Markdown("### ๐ŸŽฎ Game Setup") #
61
  ai_dropdown = gr.Dropdown(
62
  choices=self.ai_models,
63
  value=self.ai_models[0],
64
  label="Select AI Opponent"
65
- ) #
66
- ai_description = gr.Markdown(self.ai_descriptions[self.ai_models[0]]) #
67
 
68
- with gr.Row(): #
69
- rock_btn = gr.Button("๐Ÿชจ Rock", variant="secondary", elem_classes=["move-btn"]) #
70
- paper_btn = gr.Button("๐Ÿ“„ Paper", variant="secondary", elem_classes=["move-btn"]) #
71
- scissors_btn = gr.Button("โœ‚๏ธ Scissors", variant="secondary", elem_classes=["move-btn"]) #
72
-
73
- end_btn = gr.Button("End Game", variant="stop") #
74
- help_btn = gr.Button("๐Ÿ’ก Get Help", variant="primary") #
75
- help_answer_box = gr.Markdown("", visible=True) #
 
 
76
 
77
- with gr.Column(scale=2): #
78
- gr.Markdown("### ๐Ÿ“Š Game Statistics") #
79
- stats_display = gr.Markdown() #
80
- result_display = gr.Markdown("Make your move!") #
81
- end_result_display = gr.Markdown(visible=False) #
 
82
 
83
  ai_dropdown.change(
84
  fn=self.update_ai_description,
85
  inputs=[ai_dropdown],
86
  outputs=[ai_description]
87
- ) #
88
 
89
- session_id_state = gr.State("") #
 
 
90
 
91
- async def play_round_wrapper(ai_type, session_id, move):
92
- if not session_id: #
93
- session_id = f"session_{uuid.uuid4()}" #
94
- self.session_id = session_id #
95
- stats, result = await self.play_round(ai_type, move) #
96
- return stats, result, session_id #
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- # Merged and corrected JS for session handling on tab close
99
  gr.HTML("""
100
- <script>
101
- // This function will be called by Gradio's JS output
102
- function setRpsSessionId(sessionId) {
103
- if (sessionId) {
104
- localStorage.setItem('rps_session_id', sessionId);
105
- }
106
- }
107
- window.addEventListener('beforeunload', () => {
108
- const sessionId = localStorage.getItem('rps_session_id');
109
- if (sessionId) {
110
- const url = `/api/game/end`; // Use relative path for the beacon
111
- const data = JSON.stringify({ session_id: sessionId });
112
- // sendBeacon is reliable for sending data on page unload
113
- navigator.sendBeacon(url, new Blob([data], { type: 'application/json' }));
114
- localStorage.removeItem('rps_session_id');
115
- }
116
- });
117
- </script>
118
- """) #
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  def update_session_id_js(session_id):
121
- return f"setRpsSessionId('{session_id}');" #
122
 
123
- rock_btn.click(lambda ai, sid: play_round_wrapper(ai, sid, "rock"), [ai_dropdown, session_id_state], [stats_display, result_display, session_id_state], js=update_session_id_js) #
124
- paper_btn.click(lambda ai, sid: play_round_wrapper(ai, sid, "paper"), [ai_dropdown, session_id_state], [stats_display, result_display, session_id_state], js=update_session_id_js) #
125
- scissors_btn.click(lambda ai, sid: play_round_wrapper(ai, sid, "scissors"), [ai_dropdown, session_id_state], [stats_display, result_display, session_id_state], js=update_session_id_js) #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
 
127
  async def end_game(session_id):
128
- if not session_id: #
129
- return "No session to end. Play a round first!", "" #
130
- result = await self.reset_session(session_id) #
131
- return f"Game Ended: {result.get('status', 'unknown')}", gr.Markdown(visible=False) #
132
 
133
  end_btn.click(
134
- end_game,
135
  inputs=[session_id_state],
136
- outputs=[result_display, stats_display]
137
- ).then(lambda: "", outputs=[session_id_state]) #
 
138
 
 
139
  async def get_help(session_id):
140
- if not session_id: #
141
- return "Play at least one round to get a suggestion." #
142
  try:
143
- async with httpx.AsyncClient(base_url="http://127.0.0.1:7860") as client: #
144
- response = await client.post("/api/help", json={"session_id": session_id}) #
145
- response.raise_for_status()
146
- data = response.json() #
147
- return data.get("suggestion", "No suggestion available.") #
 
 
 
148
  except Exception as e:
149
- print(f"[ERROR] Getting help failed: {e}")
150
- return "Sorry, I'm having trouble getting help right now." #
 
 
 
 
151
 
152
- help_btn.click(lambda: "Generating answer, please wait...", outputs=[help_answer_box], queue=False).then(get_help, inputs=[session_id_state], outputs=[help_answer_box], queue=True) #
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- return demo #
155
 
156
  def update_ai_description(self, ai_type: str) -> str:
157
- return self.ai_descriptions[ai_type] #
158
 
159
  async def play_round(self, ai_type: str, move: str):
160
- if not self.session_id: #
161
- return "Internal Error: Session ID missing.", "Error" #
162
- result = await self.game_service.play_round(self.session_id, move, ai_type) #
163
- stats = result["stats"] #
164
  stats_text = f"""
 
165
  - Total Rounds: {stats['total_rounds']}
166
  - Player Wins: {stats['player_wins']} ({stats['player_win_rate']})
167
  - AI Wins: {stats['ai_wins']} ({stats['ai_win_rate']})
168
  - Draws: {stats['draws']}
169
 
170
- **Player Move Distribution**
171
- - Rock: {stats['player_moves']['rock']}, Paper: {stats['player_moves']['paper']}, Scissors: {stats['player_moves']['scissors']}
 
 
172
 
173
- **AI Move Distribution**
174
- - Rock: {stats['ai_moves']['rock']}, Paper: {stats['ai_moves']['paper']}, Scissors: {stats['ai_moves']['scissors']}
175
- """ #
 
 
176
  result_text = f"""
177
  ### Round Result
178
- You played: **{result['player_move'].upper()}**
179
- AI played: **{result['ai_move'].upper()}**
180
- Result: **{result['result'].replace('_', ' ').title()}**
181
- """ #
182
- return stats_text, result_text #
183
-
184
- @asynccontextmanager
185
- async def lifespan(app: FastAPI):
186
- # On startup
187
- await game_service.initialize() #
188
- print("[APP] Startup complete - LLM will initialize on first use") #
189
- yield
190
- # On shutdown
191
- await llm_service.close() #
192
- print("[APP] Shutdown complete.")
193
-
194
- # --- FastAPI App Setup ---
195
- app = FastAPI(lifespan=lifespan) #
196
- llm_service = LLMService() #
197
-
198
- @app.post("/api/help") #
199
- async def get_help_endpoint(request: Request, body: dict = Body(...)):
200
  try:
201
- session_id = body.get("session_id") #
202
- stats = None #
203
- if session_id: #
204
- session_data = game_service.cache.get_session(session_id) #
205
- if session_data and session_data['rounds']: #
206
- ai_moves = [round['ai_move'] for round in session_data['rounds']] #
207
- total_moves = len(ai_moves) #
208
- if total_moves > 0: #
 
 
209
  move_distribution = {
210
- "rock": ai_moves.count("rock"),
211
- "paper": ai_moves.count("paper"),
212
- "scissors": ai_moves.count("scissors")
213
- } #
214
- stats = {"move_distribution": move_distribution} #
215
- suggestion = await llm_service.generate_response("help", stats=stats) #
216
- return {"suggestion": suggestion} #
 
217
  except Exception as e:
218
- print(f"[ERROR] /api/help endpoint failed: {e}")
219
- return {"error": str(e), "suggestion": "Sorry, an error occurred while generating a suggestion."} #
 
 
 
 
 
 
 
220
 
221
- # Create and mount Gradio interface
222
- ui = RockPaperScissorsUI(game_service) #
223
- demo = ui.create_interface() #
 
 
224
 
225
- app.include_router(game_router, prefix="/api/game") #
226
- app = gr.mount_gradio_app(app, demo, path="/") #
 
227
 
228
  if __name__ == "__main__":
229
  import uvicorn
230
- uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True) #
 
2
  import uuid
3
  import httpx
4
  import os
 
5
 
6
  # FastAPI imports
7
  from fastapi import FastAPI, Request, Body
 
26
  # --- Gradio Interface ---
27
  class RockPaperScissorsUI:
28
  def __init__(self, game_service):
29
+ self.game_service = game_service
30
+ self.session_id = None # Will be set from State
31
+ self.ai_models = list(self.game_service.ai_models.keys())
32
  self.ai_descriptions = {
33
+ "random": "Random AI: Makes completely random moves.",
34
+ "adaptive_markov": "Adaptive Markov AI: Uses entropy-weighted Markov and frequency models to predict your next move."
35
  }
36
+ self.last_move = None
37
 
38
  async def reset_session(self, session_id: str):
39
+ # Use environment variable for base URL, fallback to localhost
40
+ base_url = os.getenv("HF_SPACE_URL", "http://localhost:7860")
41
+ async with httpx.AsyncClient() as client:
42
+ response = await client.post(
43
+ f"{base_url}/api/game/end",
44
+ json={"session_id": session_id}
45
+ )
46
+ print("[DEBUG] /api/game/end response:", response.text)
47
+ # Optionally, also clear the session in-memory (if needed)
48
+ await self.game_service.clear_session(session_id)
49
+ return {"status": "ok"}
 
 
50
 
51
  def create_interface(self):
52
+ with gr.Blocks(theme=gr.themes.Soft(), title="Rock Paper Scissors ๐ŸŽฎ") as demo:
53
+ gr.Markdown("# ๐Ÿชจ๐Ÿ“„โœ‚๏ธ Rock Paper Scissors")
54
 
55
+ with gr.Row():
56
+ with gr.Column(scale=1):
57
+ gr.Markdown("### ๐ŸŽฎ Game Setup")
58
  ai_dropdown = gr.Dropdown(
59
  choices=self.ai_models,
60
  value=self.ai_models[0],
61
  label="Select AI Opponent"
62
+ )
63
+ ai_description = gr.Markdown(self.ai_descriptions[self.ai_models[0]])
64
 
65
+ with gr.Row():
66
+ rock_btn = gr.Button("๐Ÿชจ Rock", variant="secondary", elem_classes=["move-btn"])
67
+ paper_btn = gr.Button("๐Ÿ“„ Paper", variant="secondary", elem_classes=["move-btn"])
68
+ scissors_btn = gr.Button("โœ‚๏ธ Scissors", variant="secondary", elem_classes=["move-btn"])
69
+ # Add End Game button
70
+ end_btn = gr.Button("End Game", variant="stop", elem_id="end-game-btn")
71
+ # Add Help button
72
+ help_btn = gr.Button("๐Ÿ’ก Get Help", variant="primary", elem_id="help-btn")
73
+ # Add a dedicated box for the help answer, initially empty
74
+ help_answer_box = gr.Markdown("", visible=True)
75
 
76
+ with gr.Column(scale=2):
77
+ gr.Markdown("### ๐Ÿ“Š Game Statistics")
78
+ stats_display = gr.Markdown()
79
+ result_display = gr.Markdown("Make your move!")
80
+ end_result_display = gr.Markdown(visible=False)
81
+ status_display = gr.Markdown(visible=True)
82
 
83
  ai_dropdown.change(
84
  fn=self.update_ai_description,
85
  inputs=[ai_dropdown],
86
  outputs=[ai_description]
87
+ )
88
 
89
+ # Use a Gradio State to store the session ID
90
+ move_state = gr.State("")
91
+ session_id_state = gr.State("")
92
 
93
+ async def play_rock(ai_type, session_id):
94
+ if not session_id:
95
+ session_id = f"session_{uuid.uuid4()}"
96
+ self.session_id = session_id
97
+ stats, result = await self.play_round(ai_type, "rock")
98
+ return stats, result, session_id
99
+ async def play_paper(ai_type, session_id):
100
+ if not session_id:
101
+ session_id = f"session_{uuid.uuid4()}"
102
+ self.session_id = session_id
103
+ stats, result = await self.play_round(ai_type, "paper")
104
+ return stats, result, session_id
105
+ async def play_scissors(ai_type, session_id):
106
+ if not session_id:
107
+ session_id = f"session_{uuid.uuid4()}"
108
+ self.session_id = session_id
109
+ stats, result = await self.play_round(ai_type, "scissors")
110
+ return stats, result, session_id
111
 
112
+ # Add a hidden HTML block with JS to auto-save on tab close
113
  gr.HTML("""
114
+ <script>
115
+ // Store session ID in localStorage whenever it changes
116
+ window.setRpsSessionId = function(session_id) {
117
+ if (session_id) {
118
+ localStorage.setItem('rps_session_id', session_id);
119
+ }
120
+ };
121
+ // On tab close, send session end to backend
122
+ window.onbeforeunload = function() {
123
+ let session_id = localStorage.getItem('rps_session_id');
124
+ if (session_id) {
125
+ navigator.sendBeacon("/game/end", JSON.stringify({session_id: session_id}));
126
+ }
127
+ };
128
+ </script>
129
+ """)
 
 
 
130
 
131
+ # Inject JS to click End Game button on tab close
132
+ gr.HTML("""
133
+ <script>
134
+ window.onbeforeunload = function() {
135
+ var btn = document.getElementById('end-game-btn');
136
+ if (btn) {
137
+ btn.click();
138
+ }
139
+ };
140
+ </script>
141
+ """)
142
+
143
+ # After each move, update localStorage with the session ID
144
  def update_session_id_js(session_id):
145
+ return f"window.setRpsSessionId('{session_id}');"
146
 
147
+ rock_btn.click(
148
+ fn=play_rock,
149
+ inputs=[ai_dropdown, session_id_state],
150
+ outputs=[stats_display, result_display, session_id_state],
151
+ js=update_session_id_js
152
+ )
153
+ paper_btn.click(
154
+ fn=play_paper,
155
+ inputs=[ai_dropdown, session_id_state],
156
+ outputs=[stats_display, result_display, session_id_state],
157
+ js=update_session_id_js
158
+ )
159
+ scissors_btn.click(
160
+ fn=play_scissors,
161
+ inputs=[ai_dropdown, session_id_state],
162
+ outputs=[stats_display, result_display, session_id_state],
163
+ js=update_session_id_js
164
+ )
165
 
166
+ # End Game button logic
167
  async def end_game(session_id):
168
+ if not session_id:
169
+ return "No session to end. Play a round first!"
170
+ result = await self.reset_session(session_id)
171
+ return f"End Game: {result['status']} - {result.get('message', '')}"
172
 
173
  end_btn.click(
174
+ fn=end_game,
175
  inputs=[session_id_state],
176
+ outputs=[end_result_display],
177
+ )
178
+ end_result_display.visible = True
179
 
180
+ # Help button logic
181
  async def get_help(session_id):
182
+ # Show loading message while waiting
183
+ loading_message = "Generating answer, please wait..."
184
  try:
185
+ base_url = os.getenv("HF_SPACE_URL", "http://localhost:7860")
186
+ async with httpx.AsyncClient() as client:
187
+ response = await client.post(f"{base_url}/api/help", json={"session_id": session_id})
188
+ if response.status_code == 200:
189
+ data = response.json()
190
+ suggestion = data.get("suggestion", "No suggestion available.")
191
+ else:
192
+ suggestion = "Sorry, I'm having trouble getting help right now."
193
  except Exception as e:
194
+ suggestion = f"Error getting help: {str(e)}"
195
+ return suggestion
196
+
197
+ # When help button is clicked, first show loading, then update with answer
198
+ def show_loading():
199
+ return "Generating answer, please wait..."
200
 
201
+ help_btn.click(
202
+ fn=show_loading,
203
+ inputs=[],
204
+ outputs=[help_answer_box],
205
+ queue=False
206
+ )
207
+ help_btn.click(
208
+ fn=get_help,
209
+ inputs=[session_id_state],
210
+ outputs=[help_answer_box],
211
+ queue=True
212
+ )
213
 
214
+ return demo
215
 
216
  def update_ai_description(self, ai_type: str) -> str:
217
+ return self.ai_descriptions[ai_type]
218
 
219
  async def play_round(self, ai_type: str, move: str):
220
+ if not self.session_id:
221
+ return "Internal Error: Session ID missing in play_round.", "Error"
222
+ result = await self.game_service.play_round(self.session_id, move, ai_type)
223
+ stats = result["stats"]
224
  stats_text = f"""
225
+ ### Game Statistics
226
  - Total Rounds: {stats['total_rounds']}
227
  - Player Wins: {stats['player_wins']} ({stats['player_win_rate']})
228
  - AI Wins: {stats['ai_wins']} ({stats['ai_win_rate']})
229
  - Draws: {stats['draws']}
230
 
231
+ ### Player Move Distribution
232
+ - Rock: {stats['player_moves']['rock']}
233
+ - Paper: {stats['player_moves']['paper']}
234
+ - Scissors: {stats['player_moves']['scissors']}
235
 
236
+ ### AI Move Distribution
237
+ - Rock: {stats['ai_moves']['rock']}
238
+ - Paper: {stats['ai_moves']['paper']}
239
+ - Scissors: {stats['ai_moves']['scissors']}
240
+ """
241
  result_text = f"""
242
  ### Round Result
243
+ You played: {result['player_move'].upper()}
244
+ AI played: {result['ai_move'].upper()}
245
+ Result: {result['result'].replace('_', ' ').title()}
246
+ """
247
+ return stats_text, result_text
248
+
249
+ async def clear_session(self, session_id: str):
250
+ await self.game_service.clear_session(session_id)
251
+ return {"status": "ok"}
252
+
253
+ # Create FastAPI app
254
+ app = FastAPI()
255
+
256
+ # Initialize LLM Service
257
+ llm_service = LLMService()
258
+
259
+ # Add help endpoint
260
+ @app.post("/api/help")
261
+ async def get_help(request: Request, body: dict = Body(...)):
 
 
 
262
  try:
263
+ session_id = body.get("session_id")
264
+ stats = None
265
+ if session_id:
266
+ # Try to get stats from the game service
267
+ session_data = game_service.cache.get_session(session_id)
268
+ if session_data:
269
+ # Calculate AI move distribution
270
+ ai_moves = [round['ai_move'] for round in session_data['rounds']]
271
+ total_moves = len(ai_moves)
272
+ if total_moves > 0:
273
  move_distribution = {
274
+ "rock": int((ai_moves.count("rock") / total_moves) * 100),
275
+ "paper": int((ai_moves.count("paper") / total_moves) * 100),
276
+ "scissors": int((ai_moves.count("scissors") / total_moves) * 100)
277
+ }
278
+ stats = {"move_distribution": move_distribution}
279
+ suggestion = await llm_service.generate_response("help", stats=stats)
280
+ print("Help suggestion:", suggestion) # Debug print
281
+ return {"suggestion": suggestion}
282
  except Exception as e:
283
+ return {"error": str(e), "suggestion": "Sorry, I'm having trouble generating a suggestion right now."}
284
+
285
+ # Create Gradio interface
286
+ ui = RockPaperScissorsUI(game_service)
287
+ demo = ui.create_interface()
288
+
289
+ # Mount Gradio app
290
+ app.include_router(game_router, prefix="/api/game")
291
+ app = gr.mount_gradio_app(app, demo, path="/")
292
 
293
+ # Initialize the app
294
+ @app.on_event("startup")
295
+ async def startup_event():
296
+ await game_service.initialize()
297
+ print("[APP] Startup complete - LLM will initialize on first use")
298
 
299
+ @app.on_event("shutdown")
300
+ async def shutdown_event():
301
+ await llm_service.close()
302
 
303
  if __name__ == "__main__":
304
  import uvicorn
305
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)