Jofthomas commited on
Commit
2bf4ea9
·
verified ·
1 Parent(s): 4023b86

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -511
app.py CHANGED
@@ -1,537 +1,233 @@
1
- # app_twitch_sequential.py
2
  import gradio as gr
3
  import asyncio
4
  import os
5
- # import requests # Not used currently
6
- # import json # Not used currently
7
- from typing import List, AsyncGenerator, Dict, Optional, Tuple
8
- import logging
9
- import traceback
10
- import time
11
  import random
12
- import re
 
 
13
 
14
- # --- Imports for poke_env and agents ---
15
- from poke_env.player import Player
 
16
  from poke_env import AccountConfiguration, ServerConfiguration
17
- from poke_env.environment.battle import Battle
18
-
19
- # Import your custom agent (Placeholder included)
20
- try:
21
- from agents import OpenAIAgent
22
- # from agents import GeminiAgent, MistralAgent
23
- except ImportError:
24
- print("ERROR: Could not import OpenAIAgent from agents.py. Using placeholder.")
25
- class OpenAIAgent(Player):
26
- # Placeholder Init - Ensure it mimics essential parts if needed later
27
- def __init__(self, *args, max_concurrent_battles=None, **kwargs):
28
- super().__init__(*args, max_concurrent_battles=max_concurrent_battles, **kwargs)
29
- print(f"Placeholder Agent {self.username} initialized.")
30
- self.last_error = None # Add last_error for compatibility
31
-
32
- def choose_move(self, battle: Battle):
33
- # Placeholder Move Selection
34
- print(f"Placeholder Agent {self.username}: Choosing first available move.")
35
- if battle.available_moves: return self.create_order(battle.available_moves[0])
36
- else: return self.choose_random_move(battle)
37
 
38
  # --- Configuration ---
39
- CUSTOM_SERVER_URL = "wss://jofthomas.com/showdown/websocket"
40
- CUSTOM_ACTION_URL = 'https://play.pokemonshowdown.com/action.php?'
41
- CUSTOM_BATTLE_VIEW_URL_TEMPLATE = "https://jofthomas.com/play.pokemonshowdown.com/testclient.html?nocache=true#{battle_id}"
42
- custom_config = ServerConfiguration(CUSTOM_SERVER_URL, CUSTOM_ACTION_URL)
 
 
 
43
  DEFAULT_BATTLE_FORMAT = "gen9randombattle"
44
- # NUM_INVITES_TO_ACCEPT_PER_AGENT = 1 # Now implicit in accept_challenges(None, 1)
45
-
46
- AGENT_CONFIGS = {
47
- "OpenAIAgent": {"class": OpenAIAgent, "password_env_var": "OPENAI_AGENT_PASSWORD"},
48
- "GeminiAgent": {"class": OpenAIAgent, "password_env_var": "GEMINI_AGENT_PASSWORD"},
49
- "MistralAgent": {"class": OpenAIAgent, "password_env_var": "MISTRAL_AGENT_PASSWORD"},
50
- }
51
- # Filter out agents with missing passwords at the start
52
- AVAILABLE_AGENT_NAMES = [
53
- name for name, cfg in AGENT_CONFIGS.items()
54
- if os.environ.get(cfg.get("password_env_var", ""))
55
- ]
56
- if not AVAILABLE_AGENT_NAMES:
57
- print("FATAL ERROR: No agent configurations have their required password environment variables set. Exiting.")
58
- # exit(1) # Or handle gracefully in Gradio
59
-
60
- # --- Global State Variables for Sequential Lifecycle ---
61
- active_agent_name: Optional[str] = None
62
- active_agent_instance: Optional[Player] = None
63
- active_agent_task: Optional[asyncio.Task] = None # Task for accept_challenges(None, 1)
64
- current_battle_instance: Optional[Battle] = None
65
-
66
- background_task_handle: Optional[asyncio.Task] = None # To hold the main background task
67
-
68
- # --- State variable for HTML display ---
69
- current_display_html: str = """
70
- <div style='display: flex; justify-content: center; align-items: center; height: 99vh; background-color: #eee; font-family: sans-serif;'>
71
- <p style='font-size: 1.5em;'>Initializing Stream Display...</p>
72
- </div>"""
73
- REFRESH_INTERVAL_SECONDS = 3 # Check state more frequently
74
-
75
- # --- Helper Functions ---
76
- def get_active_battle(agent: Player) -> Optional[Battle]:
77
- """Returns the first non-finished battle for an agent."""
78
- if agent and agent._battles:
79
- # Ensure agent._battles is accessed correctly
80
- active_battles = [b for b in agent._battles.values() if not b.finished]
81
- if active_battles:
82
- if active_battles[0].battle_tag: return active_battles[0]
83
- else: print(f"WARN: Found active battle for {agent.username} but it has no battle_tag yet."); return None
84
- return None
85
-
86
- def create_battle_iframe(battle_id: str) -> str:
87
- """Creates the HTML for the battle iframe."""
88
- timestamp = int(time.time() * 1000)
89
- base_template, _, fragment_template = CUSTOM_BATTLE_VIEW_URL_TEMPLATE.partition('#')
90
- formatted_fragment = fragment_template.format(battle_id=battle_id)
91
- # Add cache busting to both the URL and as a query parameter
92
- battle_url_with_query = f"{base_template}?cachebust={timestamp}#{formatted_fragment}"
93
- print(f"Generating iframe for URL: {battle_url_with_query}")
94
-
95
- # Add more attributes to help with iframe refreshing and performance
96
- return f"""
97
- <iframe
98
- src="{battle_url_with_query}"
99
- width="100%"
100
- height="99vh"
101
- style="border: none; margin: 0; padding: 0; display: block;"
102
- referrerpolicy="no-referrer"
103
- allow="autoplay; fullscreen"
104
- importance="high"
105
- loading="eager"
106
- sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
107
- onload="this.contentWindow.location.reload(true);"
108
- ></iframe>
109
  """
 
 
 
 
110
 
111
- def create_idle_html(status_message: str, instruction: str) -> str:
112
- """Creates a visually appealing idle screen HTML with improved readability and local background."""
113
- # Use the local image file served by Gradio
114
- background_image_url = "/file=pokemon_huggingface.png" # Gradio path for local files
115
-
116
- return f"""
117
- <div style="
118
- display: flex; flex-direction: column; justify-content: center; align-items: center;
119
- height: 99vh; width: 100%;
120
- background-image: url('{background_image_url}'); background-size: cover; background-position: center;
121
- border: none; margin: 0; padding: 20px;
122
- text-align: center; font-family: sans-serif; color: white;
123
- box-sizing: border-box;">
124
- <div style="background-color: rgba(0, 0, 0, 0.65); padding: 30px; border-radius: 15px; max-width: 80%;">
125
- <p style="font-size: 2.5em; margin-bottom: 20px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);">{status_message}</p>
126
- <p style="font-size: 1.5em; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);">{instruction}</p>
127
- </div>
128
- </div>"""
129
-
130
- def create_error_html(error_msg: str) -> str:
131
- """Creates HTML to display an error message."""
132
- # Using create_idle_html structure for consistency, but with error styling
133
- return f"""
134
- <div style="
135
- display: flex; flex-direction: column; justify-content: center; align-items: center;
136
- height: 99vh; width: 100%; background-color: #330000; /* Dark red background */
137
- border: none; margin: 0; padding: 20px;
138
- text-align: center; font-family: sans-serif; color: white;
139
- box-sizing: border-box;">
140
- <div style="background-color: rgba(200, 0, 0, 0.7); padding: 30px; border-radius: 15px; max-width: 80%;">
141
- <p style="font-size: 2em; margin-bottom: 20px;">An Error Occurred</p>
142
- <p style="font-size: 1.2em; color: #ffdddd;">{error_msg}</p>
143
- </div>
144
- </div>"""
145
- # --- End Helper Functions ---
146
-
147
-
148
- # --- Agent Lifecycle Management ---
149
- async def select_and_activate_new_agent():
150
- """Selects a random available agent, instantiates it, and starts its listening task."""
151
- global active_agent_name, active_agent_instance, active_agent_task, current_display_html
152
-
153
- if not AVAILABLE_AGENT_NAMES:
154
- print("Lifecycle: No available agents with passwords set.")
155
- current_display_html = create_error_html("No agents available (check password env vars).")
156
- return False # Indicate failure
157
-
158
- selected_name = random.choice(AVAILABLE_AGENT_NAMES)
159
- config = AGENT_CONFIGS[selected_name]
160
- AgentClass = config["class"]
161
- password_env_var = config["password_env_var"]
162
- agent_password = os.environ.get(password_env_var)
163
-
164
- print(f"Lifecycle: Activating agent '{selected_name}'...")
165
- current_display_html = create_idle_html("Selecting Next Agent...", f"Preparing {selected_name}...")
166
 
167
  try:
168
- account_config = AccountConfiguration(selected_name, agent_password)
169
- agent = AgentClass(
170
- account_configuration=account_config,
171
- server_configuration=custom_config,
172
- battle_format=DEFAULT_BATTLE_FORMAT,
173
- log_level=logging.INFO, # Keep INFO for debugging startup
174
- max_concurrent_battles=1
175
- )
176
- agent.last_error = None # Initialize attribute
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- # Start the task to accept exactly one battle challenge
179
- task = asyncio.create_task(agent.accept_challenges(None, 1), name=f"accept_challenge_{selected_name}")
180
- # Add callback for task completion/error
181
- task.add_done_callback(log_task_exception)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- # Update global state *after* successful creation and task launch
184
- active_agent_name = selected_name
185
- active_agent_instance = agent
186
- active_agent_task = task
187
- print(f"Lifecycle: Agent '{selected_name}' is active and listening for 1 challenge.")
188
- current_display_html = create_idle_html(f"Agent <strong>{selected_name}</strong> is ready!", f"Please challenge <strong>{selected_name}</strong> to a <strong>{DEFAULT_BATTLE_FORMAT}</strong> battle.")
189
- return True # Indicate success
190
 
191
  except Exception as e:
192
- error_msg = f"Failed to activate agent '{selected_name}': {e}"
193
- print(error_msg); traceback.print_exc()
194
- current_display_html = create_error_html(error_msg)
195
- # Ensure partial state is cleared if activation failed
196
- active_agent_name = None
197
- active_agent_instance = None
198
- active_agent_task = None
199
- return False # Indicate failure
200
-
201
- async def check_for_new_battle():
202
- """Checks if the active agent has started a battle."""
203
- global active_agent_instance, current_battle_instance
204
- if active_agent_instance:
205
- battle = get_active_battle(active_agent_instance)
206
- if battle and battle.battle_tag:
207
- print(f"Lifecycle: Agent '{active_agent_name}' started battle: {battle.battle_tag}")
208
- current_battle_instance = battle
209
- # Prevent the agent from accepting more challenges immediately
210
- # (accept_challenges(n=1) should handle this, but belt-and-suspenders)
211
- if active_agent_task and not active_agent_task.done():
212
- print(f"Lifecycle: Cancelling accept_challenges task for {active_agent_name} as battle started.")
213
- active_agent_task.cancel()
214
- # else: print(f"Lifecycle: Agent {active_agent_name} still waiting for battle...") # Too noisy maybe
215
-
216
- async def deactivate_current_agent(reason: str = "cycle"):
217
- """Cleans up the currently active agent and resets state."""
218
- global active_agent_name, active_agent_instance, active_agent_task, current_battle_instance, current_display_html
219
-
220
- print(f"Lifecycle: Deactivating agent '{active_agent_name}' (Reason: {reason})...")
221
- current_display_html = create_idle_html("Battle Finished" if reason=="battle_end" else "Resetting Agent", f"Preparing for next selection...")
222
-
223
- agent = active_agent_instance
224
- task = active_agent_task
225
-
226
- # Clear state first to prevent race conditions
227
- active_agent_name = None
228
- active_agent_instance = None
229
- active_agent_task = None
230
- current_battle_instance = None
231
-
232
- # Cancel the accept_challenges task if it's still running
233
- if task and not task.done():
234
- print(f"Lifecycle: Cancelling task for {agent.username if agent else 'unknown agent'}...")
235
- task.cancel()
236
- try:
237
- await asyncio.wait_for(task, timeout=2.0) # Give task time to process cancellation
238
- except asyncio.CancelledError:
239
- print(f"Lifecycle: Task cancellation confirmed.")
240
- except asyncio.TimeoutError:
241
- print(f"Lifecycle: Task did not confirm cancellation within timeout.")
242
- except Exception as e:
243
- print(f"Lifecycle: Error during task cancellation wait: {e}")
244
-
245
-
246
- # Disconnect the player
247
- if agent:
248
- print(f"Lifecycle: Disconnecting player {agent.username}...")
249
- try:
250
- # Check if websocket exists and is open before disconnecting
251
- if hasattr(agent, '_websocket') and agent._websocket and agent._websocket.open:
252
- await agent.disconnect()
253
- print(f"Lifecycle: Player {agent.username} disconnected.")
254
- else:
255
- print(f"Lifecycle: Player {agent.username} already disconnected or websocket not ready.")
256
- except Exception as e:
257
- print(f"ERROR during agent disconnect ({agent.username}): {e}")
258
- # Continue cleanup even if disconnect fails
259
-
260
- print(f"Lifecycle: Agent deactivated.")
261
-
262
- # --- End Agent Lifecycle Management ---
263
-
264
- # --- Main Background Task (Enhanced Logging) ---
265
- async def manage_agent_lifecycle():
266
- """Runs the main loop selecting, running, and cleaning up agents sequentially."""
267
- global current_display_html, active_agent_instance, active_agent_task, current_battle_instance
268
-
269
- print("Background lifecycle manager started.")
270
- await asyncio.sleep(2) # Initial delay
271
-
272
- loop_counter = 0
273
- while True:
274
- loop_counter += 1
275
  try:
276
- print(f"\n--- Lifecycle Check #{loop_counter} [{time.strftime('%H:%M:%S')}] ---") # Add loop counter and timestamp
277
- agent_state = f"State: Agent={active_agent_name}, TaskRunning={not active_agent_task.done() if active_agent_task else 'N/A'}, BattleMonitored={current_battle_instance is not None}"
278
- print(agent_state)
279
- previous_html_start = current_display_html[:60] # Store start of previous HTML
280
-
281
- # State 1: No agent is active
282
- if active_agent_instance is None:
283
- print(f"[{loop_counter}] State 1: No active agent. Selecting...")
284
- activated = await select_and_activate_new_agent()
285
- if not activated:
286
- print(f"[{loop_counter}] State 1: Activation failed. Waiting.")
287
- await asyncio.sleep(10); continue
288
- print(f"[{loop_counter}] State 1: Agent '{active_agent_name}' activated.")
289
- # select_and_activate_new_agent sets the initial idle HTML
290
-
291
- # State 2: Agent is active
292
- else:
293
- print(f"[{loop_counter}] State 2: Agent '{active_agent_name}' active.")
294
- # Check task status first
295
- if active_agent_task and active_agent_task.done():
296
- # ... (error/completion handling as before) ...
297
- print(f"[{loop_counter}] State 2: Task done/error. Deactivating {active_agent_name}.")
298
- await deactivate_current_agent(reason="task_done_or_error"); continue
299
-
300
- # Check for battle start if not monitoring
301
- if current_battle_instance is None:
302
- print(f"[{loop_counter}] State 2: Checking for new battle...")
303
- await check_for_new_battle()
304
- if current_battle_instance:
305
- print(f"[{loop_counter}] State 2: *** NEW BATTLE DETECTED: {current_battle_instance.battle_tag} ***")
306
- # else: print(f"[{loop_counter}] State 2: No new battle detected.")
307
-
308
- # State 2a: Battle is monitored
309
- if current_battle_instance is not None:
310
- battle_tag = current_battle_instance.battle_tag
311
- print(f"[{loop_counter}] State 2a: Monitoring battle {battle_tag}")
312
- battle_obj_current = active_agent_instance._battles.get(battle_tag)
313
-
314
- if battle_obj_current:
315
- is_finished = battle_obj_current.finished
316
- print(f"[{loop_counter}] State 2a: Battle object found. Finished: {is_finished}")
317
- if not is_finished:
318
- print(f"[{loop_counter}] State 2a: Battle {battle_tag} IN PROGRESS. Setting iframe HTML.")
319
- # Every 3 cycles (based on loop_counter), regenerate the iframe HTML to ensure fresh content
320
- if loop_counter % 3 == 0:
321
- current_display_html = create_battle_iframe(battle_tag)
322
- print(f"[{loop_counter}] Regenerated iframe HTML (periodic refresh)")
323
- # Add check immediately after setting
324
- if "iframe" not in current_display_html: print(f"[{loop_counter}] ***ERROR: IFRAME not found in generated HTML!***")
325
-
326
- else:
327
- print(f"[{loop_counter}] State 2a: Battle {battle_tag} FINISHED. Deactivating agent.")
328
- await deactivate_current_agent(reason="battle_end")
329
- await asyncio.sleep(5); continue
330
- else:
331
- print(f"[{loop_counter}] State 2a WARNING: Battle object for {battle_tag} MISSING! Deactivating.")
332
- await deactivate_current_agent(reason="battle_object_missing"); continue
333
-
334
- # State 2b: Agent active, no battle (listening)
335
- else:
336
- print(f"[{loop_counter}] State 2b: Agent {active_agent_name} LISTENING. Setting idle HTML.")
337
- current_display_html = create_idle_html(f"Agent <strong>{active_agent_name}</strong> is ready!", f"Please challenge <strong>{active_agent_name}</strong> to a <strong>{DEFAULT_BATTLE_FORMAT}</strong> battle.")
338
- if "is ready" not in current_display_html: print(f"[{loop_counter}] ***ERROR: IDLE text not found in generated HTML!***")
339
-
340
- # --- HTML Change Check ---
341
- new_html_start = current_display_html[:60]
342
- if new_html_start != previous_html_start:
343
- print(f"[{loop_counter}] HTML CHANGED! Type: {type(current_display_html)}, New HTML starts: {new_html_start}...")
344
- else:
345
- print(f"[{loop_counter}] HTML Unchanged. Starts: {new_html_start}...")
346
- # --- End HTML Change Check ---
347
-
348
- print(f"[{loop_counter}] State End. Sleeping {REFRESH_INTERVAL_SECONDS}s.")
349
-
350
- except Exception as e:
351
- print(f"ERROR in main lifecycle loop #{loop_counter}: {e}")
352
- traceback.print_exc()
353
- if active_agent_instance: await deactivate_current_agent(reason="main_loop_error")
354
- else: current_display_html = create_error_html(f"Error in lifecycle manager: {e}")
355
- await asyncio.sleep(10)
356
-
357
- await asyncio.sleep(REFRESH_INTERVAL_SECONDS)
358
-
359
-
360
- # --- Gradio Update Function (Enhanced Logging) ---
361
- def update_viewer_from_state():
362
- """Called by Gradio (triggered by JS click) to update the HTML component."""
363
- global current_display_html
364
- # Use a more unique identifier for the HTML content type
365
- html_indicator = "iframe" if "<iframe" in current_display_html[:100].lower() else "idle/other"
366
-
367
- # For iframe content, add a timestamp to force refresh
368
- if html_indicator == "iframe":
369
- # Extract the current URL from the iframe
370
- url_match = re.search(r'src="([^"]+)"', current_display_html)
371
- if url_match:
372
- original_url = url_match.group(1)
373
- # Update the timestamp in the URL
374
- updated_url = re.sub(r'cachebust=\d+', f'cachebust={int(time.time() * 1000)}', original_url)
375
- # Replace the URL in the iframe HTML
376
- current_display_html = current_display_html.replace(original_url, updated_url)
377
-
378
- print(f"Gradio Trigger [{time.strftime('%H:%M:%S')}]: Updating viewer. Current HTML type: {html_indicator}. Starts: {current_display_html[:100]}...")
379
- return gr.update(value=current_display_html)
380
-
381
-
382
- async def start_background_tasks():
383
- """Creates and stores the background monitor task."""
384
- global background_task_handle
385
- if not background_task_handle or background_task_handle.done():
386
- print("Launching background lifecycle manager task...")
387
- # Start the new lifecycle manager task
388
- background_task_handle = asyncio.create_task(manage_agent_lifecycle(), name="lifecycle_manager")
389
- background_task_handle.add_done_callback(log_task_exception)
390
- else:
391
- print("Background lifecycle manager task already running.")
392
-
393
- def log_task_exception(task: asyncio.Task):
394
- """Callback to log exceptions from background tasks."""
395
  try:
396
- if task.cancelled():
397
- print(f"Task {task.get_name()} was cancelled.")
398
- return
399
- task.result() # Raises exception if task failed
400
- print(f"Task {task.get_name()} finished cleanly.")
401
- except asyncio.CancelledError:
402
- # Logged above
403
- pass
404
  except Exception as e:
405
- print(f"Exception in background task {task.get_name()}: {e}")
406
- traceback.print_exc()
407
- # Potentially trigger a reset or error display state here
408
- # global current_display_html
409
- # current_display_html = create_error_html(f"Error in {task.get_name()}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
 
 
 
411
  def main_app():
412
- print("Defining Gradio UI (Twitch Mode - Sequential Lifecycle)...")
413
- css = "body {padding: 0 !important; margin: 0 !important;} .gradio-container {max-width: none !important;}"
414
-
415
- with gr.Blocks(title="Pokemon Showdown Stream", css=css) as demo:
416
- viewer_html = gr.HTML(current_display_html)
417
- refresh_trigger_btn = gr.Button("Refresh Internally", visible=False, elem_id="refresh_trigger_btn_id")
418
-
419
- # JS click interval needs to be faster or equal to REFRESH_INTERVAL_SECONDS
420
- js_click_script = f"""
421
- <script>
422
- function triggerRefresh() {{
423
- var btn = document.getElementById('refresh_trigger_btn_id');
424
- if (btn) {{
425
- // console.log('JS: Clicking hidden refresh button'); // Reduce noise
426
- btn.click();
427
- }} else {{ console.error('JS: Could not find refresh button'); }}
428
-
429
- // Also try to directly refresh any iframes on the page
430
- const iframes = document.querySelectorAll('iframe');
431
- iframes.forEach(iframe => {{
432
- try {{
433
- // Try to reload the iframe content
434
- if (iframe.contentWindow) {{
435
- // Add timestamp to src to force refresh
436
- const currentSrc = iframe.src;
437
- if (currentSrc && currentSrc.includes('cachebust=')) {{
438
- const newSrc = currentSrc.replace(/cachebust=\\d+/, `cachebust=${{Date.now()}}`);
439
- iframe.src = newSrc;
440
- }}
441
- // Also try to reload the content window
442
- if (iframe.contentWindow.location) {{
443
- iframe.contentWindow.location.reload(true);
444
- }}
445
- }}
446
- }} catch (e) {{
447
- // Silently catch errors (might be cross-origin issues)
448
- }}
449
- }});
450
- }}
451
- const refreshInterval = setInterval(triggerRefresh, {int(REFRESH_INTERVAL_SECONDS * 1000)});
452
- </script>
453
- """
454
- _ = gr.Markdown(js_click_script, visible=False)
455
-
456
- # Event Handlers
457
- # 1. Start the background lifecycle manager task ONCE on load
458
- demo.load(start_background_tasks, inputs=None, outputs=None)
459
- # 2. When the hidden button is clicked (by JS), update the HTML viewer
460
- refresh_trigger_btn.click(update_viewer_from_state, inputs=None, outputs=viewer_html)
461
-
462
- print("Gradio UI defined. Background task and JS trigger configured.")
463
  return demo
464
- # --- End Gradio UI ---
465
 
466
- # --- Main execution block ---
 
467
  if __name__ == "__main__":
468
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
469
- logging.getLogger('poke_env').setLevel(logging.WARNING)
470
- print("Starting application (Twitch Mode - Sequential Lifecycle)..."); print("="*50)
471
-
472
- if not AVAILABLE_AGENT_NAMES:
473
- print("FATAL: No agents found with configured passwords. Please set environment variables:")
474
- for name, cfg in AGENT_CONFIGS.items(): print(f"- {cfg.get('password_env_var', 'N/A')} (for agent: {name})")
475
- print("="*50)
476
- # Prevent Gradio from starting if no agents can run
477
- exit("Exiting due to missing agent passwords.")
478
- else:
479
- print("Found available agents:")
480
- for name in AVAILABLE_AGENT_NAMES: print(f"- {name}")
481
- print("="*50)
482
-
483
  app = main_app()
484
- print("Launching Gradio app...")
485
-
486
- try:
487
- app.queue().launch(share=False, server_name="0.0.0.0")
488
- finally:
489
- print("\nGradio app shut down.")
490
- print("Attempting to cancel tasks...")
491
-
492
- # --- Modified Cleanup for Sequential Model ---
493
- async def cleanup_tasks():
494
- global background_task_handle
495
- tasks_to_cancel = []
496
- agent_to_disconnect = active_agent_instance # Get current agent before potentially clearing state
497
-
498
- # Add main lifecycle manager task
499
- if background_task_handle and not background_task_handle.done():
500
- tasks_to_cancel.append(background_task_handle)
501
- # Add active agent's accept_challenges task if it exists and running
502
- if active_agent_task and not active_agent_task.done():
503
- tasks_to_cancel.append(active_agent_task)
504
-
505
- if tasks_to_cancel:
506
- print(f"Sending cancel signal to {len(tasks_to_cancel)} tasks...")
507
- for task in tasks_to_cancel: task.cancel()
508
- results = await asyncio.gather(*tasks_to_cancel, return_exceptions=True)
509
- print(f"Finished waiting for task cancellation. Results: {results}")
510
-
511
- # Attempt disconnect on the potentially active agent
512
- if agent_to_disconnect:
513
- print(f"Attempting disconnect for last active agent {agent_to_disconnect.username}...")
514
- if hasattr(agent_to_disconnect, 'disconnect') and callable(agent_to_disconnect.disconnect):
515
- try:
516
- if hasattr(agent_to_disconnect, '_websocket') and agent_to_disconnect._websocket and agent_to_disconnect._websocket.open:
517
- await agent_to_disconnect.disconnect()
518
- print(f"Agent {agent_to_disconnect.username} disconnected.")
519
- else: print(f"Agent {agent_to_disconnect.username} already disconnected or websocket not ready.")
520
- except Exception as e: print(f"Error during cleanup disconnect for {agent_to_disconnect.username}: {e}")
521
- else:
522
- print("No active agent instance to disconnect during cleanup.")
523
-
524
- print("Cleanup attempt finished.")
525
- # --- End Modified Cleanup ---
526
-
527
- try:
528
- loop = asyncio.get_running_loop()
529
- loop.create_task(cleanup_tasks())
530
- time.sleep(3) # Allow time for cleanup signals
531
- except RuntimeError:
532
- try: asyncio.run(cleanup_tasks())
533
- except Exception as e: print(f"Could not run final async cleanup: {e}")
534
- except Exception as e:
535
- print(f"An unexpected error occurred during cleanup: {e}")
536
-
537
- print("Application finished.")
 
1
+ # app.py
2
  import gradio as gr
3
  import asyncio
4
  import os
 
 
 
 
 
 
5
  import random
6
+ import traceback
7
+ import logging
8
+ import threading # Import threading
9
 
10
+ # --- [ Previous code for imports, configuration, logging setup remains the same ] ---
11
+ # Import poke-env components
12
+ from poke_env.player import Player, RandomPlayer
13
  from poke_env import AccountConfiguration, ServerConfiguration
14
+ # Import your custom agent(s)
15
+ from agents import OpenAIAgent # Assuming agents.py exists with OpenAIAgent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # --- Configuration ---
18
+ POKE_SERVER_URL = "wss://jofthomas.com/showdown/websocket"
19
+ POKE_AUTH_URL = "https://jofthomas.com/showdown/action.php"
20
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s')
21
+
22
+ # --- Constants ---
23
+ RANDOM_PLAYER_BASE_NAME = "RandAgent"
24
+ OPENAI_AGENT_BASE_NAME = "OpenAIAgent"
25
  DEFAULT_BATTLE_FORMAT = "gen9randombattle"
26
+ custom_config = ServerConfiguration(POKE_SERVER_URL, POKE_AUTH_URL)
27
+
28
+
29
+ # --- Agent Creation (Async - Required by poke-env) ---
30
+ # [ create_agent_async function remains exactly the same as the previous version ]
31
+ async def create_agent_async(agent_type: str, battle_format: str = DEFAULT_BATTLE_FORMAT) -> Player | str:
32
+ """
33
+ Creates and initializes a SINGLE agent instance with a unique username.
34
+ This function MUST be async because Player initialization involves async network setup.
35
+ Returns the Player object on success, or an error string on failure.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
37
+ logging.info(f"Attempting to create agent of type: {agent_type}")
38
+ player: Player | None = None
39
+ error_message: str | None = None
40
+ username: str = "unknown_agent" # Default for logging in case of early failure
41
 
42
+ agent_suffix = random.randint(10000, 999999)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  try:
45
+ if agent_type == "Random Player":
46
+ username = f"{RANDOM_PLAYER_BASE_NAME}{agent_suffix}"
47
+ account_config = AccountConfiguration(username, None)
48
+ logging.info(f"Initializing RandomPlayer with username: {username}")
49
+ player = RandomPlayer(
50
+ account_configuration=account_config,
51
+ server_configuration=custom_config,
52
+ battle_format=battle_format,
53
+ start_listening=True,
54
+ )
55
+ elif agent_type == "OpenAI Agent":
56
+ if not os.getenv("OPENAI_API_KEY"):
57
+ error_message = "Error: Cannot create OpenAI Agent. OPENAI_API_KEY environment variable is missing."
58
+ logging.error(error_message)
59
+ return error_message
60
+ username = f"{OPENAI_AGENT_BASE_NAME}{agent_suffix}"
61
+ account_config = AccountConfiguration(username, None)
62
+ logging.info(f"Initializing OpenAIAgent with username: {username}")
63
+ player = OpenAIAgent(
64
+ account_configuration=account_config,
65
+ server_configuration=custom_config,
66
+ battle_format=battle_format,
67
+ start_listening=True,
68
+ )
69
+ else:
70
+ error_message = f"Error: Invalid agent type '{agent_type}' requested."
71
+ logging.error(error_message)
72
+ return error_message
73
+
74
+ logging.info(f"Agent object ({username}) created successfully.")
75
+ return player
76
 
77
+ except Exception as e:
78
+ error_message = f"Error creating agent {username}: {e}"
79
+ logging.error(error_message)
80
+ logging.error(traceback.format_exc())
81
+ return error_message
82
+
83
+ # --- Battle Invitation (Async - Required by poke-env) ---
84
+ # [ send_battle_invite_async function remains exactly the same as the previous version ]
85
+ async def send_battle_invite_async(player: Player, opponent_username: str, battle_format: str) -> str:
86
+ """
87
+ Sends a challenge using the provided player object.
88
+ This function MUST be async as sending challenges involves network I/O.
89
+ Returns a status string (success or error message).
90
+ """
91
+ if not isinstance(player, Player):
92
+ err_msg = f"Internal Error: Invalid object passed instead of Player: {type(player)}"
93
+ logging.error(err_msg)
94
+ # In background thread, we might just log this and exit thread
95
+ raise TypeError(err_msg) # Raise exception to be caught by the thread runner
96
+
97
+ player_username = getattr(player, 'username', 'unknown_agent')
98
 
99
+ try:
100
+ logging.info(f"Attempting to send challenge from {player_username} to {opponent_username} in format {battle_format}")
101
+ await player.send_challenges(opponent_username, n_challenges=1)
102
+ success_msg = f"Battle invitation ({battle_format}) sent to '{opponent_username}' from bot '{player_username}'."
103
+ logging.info(success_msg)
104
+ return success_msg # Indicate success
 
105
 
106
  except Exception as e:
107
+ error_msg = f"Error sending challenge from {player_username} to {opponent_username}: {e}"
108
+ logging.error(error_msg)
109
+ logging.error(traceback.format_exc())
110
+ # Re-raise or return error indication for the thread runner
111
+ raise e # Raise exception to be caught by the thread runner
112
+
113
+
114
+ # --- Background Task Runner (Runs in a separate thread) ---
115
+ def run_invite_in_background(agent_choice: str, target_username: str, battle_format: str):
116
+ """
117
+ This function runs in a separate thread for each invite request.
118
+ It sets up and runs the asyncio operations needed for one invite.
119
+ """
120
+ thread_name = threading.current_thread().name
121
+ logging.info(f"Background thread '{thread_name}' started for {agent_choice} vs {target_username}.")
122
+
123
+ async def _run_async_challenge_steps():
124
+ """The async steps to be run via asyncio.run() in this thread."""
125
+ agent_or_error = await create_agent_async(agent_choice, battle_format)
126
+
127
+ if isinstance(agent_or_error, str):
128
+ # Agent creation failed, log the error message from create_agent_async
129
+ logging.error(f"[{thread_name}] Agent creation failed: {agent_or_error}")
130
+ # No further action needed in this thread
131
+ return
132
+
133
+ player_instance: Player = agent_or_error
134
+ player_username = getattr(player_instance, 'username', 'agent')
135
+ logging.info(f"[{thread_name}] Agent {player_username} created, proceeding to challenge {target_username}.")
136
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  try:
138
+ result = await send_battle_invite_async(player_instance, target_username, battle_format)
139
+ # Log the success message from send_battle_invite_async
140
+ logging.info(f"[{thread_name}] Challenge result: {result}")
141
+ except Exception as invite_error:
142
+ # Log errors from send_battle_invite_async
143
+ # Error message/traceback already logged inside send_battle_invite_async
144
+ logging.error(f"[{thread_name}] Failed to send challenge from {player_username} to {target_username}. Error: {invite_error}")
145
+ finally:
146
+ pass
147
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  try:
149
+ asyncio.run(_run_async_challenge_steps())
150
+ logging.info(f"Background thread '{thread_name}' finished successfully for {target_username}.")
151
+ except RuntimeError as e:
152
+ logging.error(f"[{thread_name}] asyncio RuntimeError: {e}")
153
+ logging.error(traceback.format_exc())
 
 
 
154
  except Exception as e:
155
+ logging.error(f"[{thread_name}] Unexpected error in background task: {e}")
156
+ logging.error(traceback.format_exc())
157
+
158
+ # --- Gradio Interface Logic (Starts the background thread) ---
159
+ def start_invite_thread(agent_choice: str, username: str) -> str:
160
+ """
161
+ Handles the Gradio button click (Synchronous, but FAST).
162
+ Performs basic validation and starts a background thread to handle
163
+ the actual agent creation and invitation process.
164
+ Returns an immediate status message to Gradio.
165
+ """
166
+ username_clean = username.strip()
167
+ if not username_clean:
168
+ return "⚠️ Please enter your Showdown username."
169
+ if not agent_choice:
170
+ return "⚠️ Please select an agent type."
171
+
172
+ logging.info(f"Received request: Agent={agent_choice}, Opponent={username_clean}. Starting background thread.")
173
+
174
+ # Create and start the background thread
175
+ thread = threading.Thread(
176
+ target=run_invite_in_background,
177
+ args=(agent_choice, username_clean, DEFAULT_BATTLE_FORMAT),
178
+ daemon=True # Set as daemon so threads don't block app exit
179
+ )
180
+ thread.start()
181
+
182
+ # Return immediately to Gradio UI
183
+ return f"✅ Invite process for '{username_clean}' started in background. Check Pokémon Showdown and logs for status."
184
 
185
+
186
+ # --- Gradio UI Definition ---
187
+ # [ main_app function remains the same, but the button click now calls start_invite_thread ]
188
  def main_app():
189
+ """Creates and returns the Gradio application interface."""
190
+
191
+ agent_options = ["Random Player"]
192
+ agent_options.append("OpenAI Agent")
193
+
194
+ # Use a more descriptive title if possible
195
+ with gr.Blocks(title="Pokemon Showdown Multi-Challenger") as demo:
196
+ gr.Markdown("# Pokémon Battle Agent Challenger")
197
+ gr.Markdown(
198
+ "1. Choose a name in the Iframe, if you have an account, you can also connect.\n"
199
+ "2. Select an agent type.\n"
200
+ "3. Enter **your** Showdown username (the one you are logged in with below).\n"
201
+ "4. Click 'Send Battle Invitation'. You can click multiple times for different users.\n\n"
202
+ "A temporary bot will be created *in the background* to send the challenge in `gen9randombattle` format."
203
+ )
204
+
205
+ with gr.Row():
206
+ agent_dropdown = gr.Dropdown(
207
+ label="Select Agent", choices=agent_options, value=agent_options[0], scale=1
208
+ )
209
+ name_input = gr.Textbox(
210
+ label="Your Pokémon Showdown Username", placeholder="Enter username used in Showdown below", scale=2
211
+ )
212
+ #variant="primary"
213
+ battle_button = gr.Button("Send Battle Invitation", scale=1)
214
+ gr.HTML("""
215
+ <iframe
216
+ src="https://jofthomas.com/play.pokemonshowdown.com/testclient.html"
217
+ width="100%" height="800" style="border: none;" referrerpolicy="no-referrer">
218
+ </iframe>
219
+ """)
220
+
221
+ # *** IMPORTANT: Update the click handler ***
222
+ battle_button.click(
223
+ fn=start_invite_thread, # Calls the function that starts the thread
224
+ inputs=[agent_dropdown, name_input],
225
+ )
226
+
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  return demo
 
228
 
229
+ # --- Application Entry Point ---
230
+ # [ if __name__ == "__main__": block remains the same ]
231
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  app = main_app()
233
+ app.launch()