Jofthomas commited on
Commit
9f27129
·
verified ·
1 Parent(s): 983006a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -188
app.py CHANGED
@@ -1,265 +1,257 @@
1
  # app.py
2
  import gradio as gr
3
  import asyncio
4
- import threading
5
  import os
6
- import random # <-- Import the random module
 
7
 
8
  # Import poke-env components
9
  from poke_env.player import Player, RandomPlayer
10
- from poke_env import AccountConfiguration,ServerConfiguration
11
  # Import your custom agent
12
- from agents import OpenAIAgent
13
-
14
-
15
- # --- Global variables for players and thread ---
16
- random_player: Player | None = None
17
- openai_agent: Player | None = None
18
- agent_init_thread: threading.Thread | None = None
19
- init_lock = threading.Lock() # To prevent race conditions during init
20
- agents_initialized = False
21
 
22
  # --- Configuration ---
23
- custom_config = ServerConfiguration(
24
- "wss://jofthomas.com/showdown/websocket", # WebSocket URL
25
- "https://jofthomas.com/showdown/action.php" # Authentication URL
26
- )
 
 
 
27
 
28
  # --- Dynamic Account Configuration ---
29
- # Define base names for the bots
30
- RANDOM_PLAYER_BASE_NAME = "RandomAgent"
31
- OPENAI_AGENT_BASE_NAME = "OpenAIAgent" # You can change this if you like
32
-
33
  DEFAULT_BATTLE_FORMAT = "gen9randombattle"
34
 
35
- # --- Agent Initialization ---
36
- def initialize_agents_sync():
37
- """Initializes both player agents in a background thread."""
38
- global random_player, openai_agent, agents_initialized
39
-
40
- # Generate random suffixes (4-6 digits, i.e., 1000 to 999999)
41
- random_player_suffix = random.randint(1000, 999999)
42
- openai_agent_suffix = random.randint(1000, 999999)
43
-
44
-
45
- # Construct full usernames
46
- random_player_username = f"{RANDOM_PLAYER_BASE_NAME}{random_player_suffix}"
47
- openai_agent_username = f"{OPENAI_AGENT_BASE_NAME}{openai_agent_suffix}"
48
-
49
- # Create AccountConfiguration objects with dynamic usernames and no password (guest)
50
- random_account_config = AccountConfiguration(random_player_username, None)
51
- openai_account_config = AccountConfiguration(openai_agent_username, None)
52
-
53
- # Optional: Print the generated usernames for confirmation when the script starts
54
- print(f"Using RandomPlayer username: {random_account_config.username}")
55
- print(f"Using OpenAIAgent username: {openai_account_config.username}")
56
-
57
- # Ensure this runs only once
58
- with init_lock:
59
- if agents_initialized:
60
- print("Agents already initialized.")
61
- return
62
- print("Initializing agents...")
63
- # We need an event loop in this new thread for player initialization
64
- loop = asyncio.new_event_loop()
65
- asyncio.set_event_loop(loop)
66
- try:
67
- # Initialize Random Player - PASSING THE CONFIGURATION
68
- # Use the dynamically generated username from random_account_config
69
- print(f"Initializing RandomPlayer ({random_account_config.username})...")
70
- random_player = RandomPlayer(
71
- account_configuration=random_account_config, # <-- Pass the config
72
  server_configuration=custom_config,
73
- battle_format=DEFAULT_BATTLE_FORMAT
 
 
74
  )
75
- print("RandomPlayer initialized.")
 
 
 
 
 
 
 
76
 
77
- # Initialize OpenAI Agent - PASSING THE CONFIGURATION
78
- # Use the dynamically generated username from openai_account_config
79
- print(f"Initializing OpenAIAgent ({openai_account_config.username})...")
80
- openai_agent = OpenAIAgent(
81
- account_configuration=openai_account_config, # <-- Pass the config
82
  server_configuration=custom_config,
83
- battle_format=DEFAULT_BATTLE_FORMAT
 
84
  )
85
- print("OpenAIAgent initialized.")
86
-
87
- agents_initialized = True
88
- print("Agent initialization complete.")
89
- except Exception as e:
90
- print(f"!!! Error during agent initialization: {e}")
91
- # Reset globals if init fails partially
92
- random_player = None
93
- openai_agent = None
94
- agents_initialized = False
95
- # Note: Don't close the loop here, the players might need it running implicitly
96
- # loop.close()
97
-
98
- # Function to start the initialization thread
99
- def start_agent_initialization():
100
- """Starts the agent initialization thread if not already running."""
101
- global agent_init_thread
102
- if not agents_initialized and (agent_init_thread is None or not agent_init_thread.is_alive()):
103
- print("Starting agent initialization thread...")
104
- agent_init_thread = threading.Thread(target=initialize_agents_sync, daemon=True)
105
- agent_init_thread.start()
106
- elif agents_initialized:
107
- print("Agents already initialized, no need to start thread.")
108
- else:
109
- print("Initialization thread already running.")
110
-
111
- # --- Battle Invitation Logic ---
112
- # Using player.username SHOULD automatically use the assigned username
113
- async def send_battle_invite_async(player: Player, username: str, battle_format: str = DEFAULT_BATTLE_FORMAT):
 
 
114
  """Sends a challenge using the provided player object."""
115
- if player is None:
116
- return "Error: The selected player is not available/initialized."
117
- if not username or not username.strip():
118
- return "Error: Please enter a valid Showdown username."
 
 
 
119
  try:
120
- # player.username should reflect the name set in AccountConfiguration
121
- print(f"Attempting to send challenge from {player.username} to {username} in format {battle_format}")
122
- print(player) # Keep this for debugging if needed
123
- # Pass the battle_format if you want it to be different from player's default
124
- await player.send_challenges(username, n_challenges=1) # Pass format here
125
- return f"Battle invitation ({battle_format}) sent to {username} from bot {player.username}! Check Showdown."
 
 
126
  except Exception as e:
127
- # Log the full error for debugging
128
- import traceback
129
- # Make sure player.username exists even if there's an error before login completes
130
- player_name = getattr(player, 'username', 'unknown player')
131
- print(f"Error sending challenge from {player_name}:")
132
- traceback.print_exc() # Print stack trace
133
- return f"Error sending challenge: {str(e)}. Check console logs."
134
-
135
- # Wrapper for the async function to use in Gradio, handling agent selection
136
- def invite_to_battle(agent_choice: str, username: str):
137
- """Selects the agent and initiates the battle invitation."""
138
- global random_player, openai_agent, agents_initialized
139
-
140
- if not agents_initialized:
141
- # Try to initialize if the button is clicked before init finishes
142
- start_agent_initialization()
143
- return "Agents are initializing, please wait a few seconds and try again."
144
-
145
- selected_player: Player | None = None
146
- if agent_choice == "Random Player":
147
- selected_player = random_player
148
- if selected_player is None:
149
- return "Error: Random Player is not available. Initialization might have failed."
150
- elif agent_choice == "OpenAI Agent":
151
- selected_player = openai_agent
152
- if selected_player is None:
153
- # Check if API key might be the issue
154
- if not os.getenv("OPENAI_API_KEY"):
155
- return "Error: OpenAI Agent not available. OPENAI_API_KEY is missing."
156
- else:
157
- return "Error: OpenAI Agent not available. Initialization might have failed. Check logs."
158
- else:
159
- return "Error: Invalid agent choice selected."
160
 
161
- # Ensure username is provided
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  username_clean = username.strip()
163
  if not username_clean:
164
  return "Please enter your Showdown username."
 
 
165
 
166
- # Run the async challenge function in a managed event loop
167
- try:
168
- # Try to get the existing loop from the init thread or create a new one
169
- try:
170
- loop = asyncio.get_running_loop()
171
- except RuntimeError:
172
- print("No running event loop, creating new one for challenge.")
173
- loop = asyncio.new_event_loop()
174
- asyncio.set_event_loop(loop)
175
-
176
- # If the main loop is running, schedule the task; otherwise, run until complete
177
- if loop.is_running():
178
- # Schedule the coroutine and wait for its result
179
- future = asyncio.run_coroutine_threadsafe(
180
- # Pass the default battle format explicitly if needed
181
- send_battle_invite_async(selected_player, username_clean, DEFAULT_BATTLE_FORMAT),
182
- loop
183
- )
184
- result = future.result(timeout=30) # Add a timeout
185
- else:
186
- result = loop.run_until_complete(
187
- # Pass the default battle format explicitly if needed
188
- send_battle_invite_async(selected_player, username_clean, DEFAULT_BATTLE_FORMAT)
189
- )
190
 
 
 
 
 
 
191
  return result
192
- except TimeoutError:
193
- return "Error: Sending challenge timed out."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  except Exception as e:
195
- print(f"Unexpected error in invite_to_battle's async execution: {e}")
196
- import traceback
197
  traceback.print_exc()
198
- return f"An unexpected error occurred: {e}"
199
 
200
 
201
- # --- Gradio UI Definition ---
202
- # iframe code to embed Pokemon Showdown (using official URL)
203
  iframe_code = """
204
  <iframe
205
- src="https://jofthomas.com/play.pokemonshowdown.com/testclient.html"
206
  width="100%"
207
  height="800"
208
  style="border: none;"
209
  referrerpolicy="no-referrer">
210
  </iframe>
211
  """
 
 
212
 
213
  def main_app():
214
  """Creates and returns the Gradio application interface."""
215
- # Start agent initialization when the app is defined/loaded
216
- start_agent_initialization()
217
 
218
- # Using gr.Blocks. The default layout should stretch reasonably wide.
219
  with gr.Blocks(title="Pokemon Showdown Agent") as demo:
220
  gr.Markdown("# Pokémon Battle Agent")
221
  gr.Markdown(
222
  "Select an agent, enter **your** Showdown username "
223
- "(the one you are logged in with below), and click Send Invite."
224
- "Note : you can just select a random set of character as long as someone registered does not already use this username"
225
  )
226
 
227
- # --- Row for Controls at the Top ---
228
  with gr.Row():
229
- # Place controls here
230
  agent_dropdown = gr.Dropdown(
231
  label="Select Agent",
232
  choices=["Random Player", "OpenAI Agent"],
233
  value="Random Player",
234
- scale=1 # Give dropdown reasonable space
235
  )
236
  name_input = gr.Textbox(
237
  label="Your Pokémon Showdown Username",
238
  placeholder="Enter username used in Showdown below",
239
- scale=2 # Give name input more relative space
240
  )
241
- battle_button = gr.Button("Send Battle Invitation", scale=1) # Button takes less space
242
 
 
 
243
 
244
- # --- Section for the IFrame below the controls ---
245
  gr.Markdown("### Pokémon Showdown Interface")
246
  gr.Markdown("Log in/use the username you entered above.")
247
- # The HTML component containing the iframe will take up the available width
248
  gr.HTML(iframe_code)
249
 
250
- # --- Connect button click to the invitation function ---
251
  battle_button.click(
252
  fn=invite_to_battle,
253
  inputs=[agent_dropdown, name_input],
254
-
255
  )
256
 
257
  return demo
258
 
259
  # --- Main execution block ---
260
  if __name__ == "__main__":
261
- # Create and launch the Gradio app
 
 
 
 
 
262
  app = main_app()
263
- # You might need to configure server_name and server_port depending on your deployment environment
264
- # Use app.launch(share=True) for a public link (if needed and safe)
265
- app.launch() # server_name="0.0.0.0" # To make accessible on network
 
1
  # app.py
2
  import gradio as gr
3
  import asyncio
4
+ import threading # Still needed for asyncio.run in sync context potentially
5
  import os
6
+ import random
7
+ import traceback # For detailed error logging
8
 
9
  # Import poke-env components
10
  from poke_env.player import Player, RandomPlayer
11
+ from poke_env import AccountConfiguration, ServerConfiguration
12
  # Import your custom agent
13
+ from agents import OpenAIAgent # Assuming agents.py exists with OpenAIAgent
 
 
 
 
 
 
 
 
14
 
15
  # --- Configuration ---
16
+ # Use your custom server or the official Smogon server
17
+ # custom_config = ServerConfiguration(
18
+ # "wss://jofthomas.com/showdown/websocket", # WebSocket URL
19
+ # "https://jofthomas.com/showdown/action.php" # Authentication URL
20
+ # )
21
+ # Or use the default Smogon server configuration
22
+ custom_config = ServerConfiguration.get_default() # Easier for general use
23
 
24
  # --- Dynamic Account Configuration ---
25
+ RANDOM_PLAYER_BASE_NAME = "TempRandAgent" # Changed base name slightly
26
+ OPENAI_AGENT_BASE_NAME = "TempOpenAIAgent" # Changed base name slightly
 
 
27
  DEFAULT_BATTLE_FORMAT = "gen9randombattle"
28
 
29
+ # --- Agent Creation (Per Request) ---
30
+ async def create_agent_async(agent_type: str, battle_format: str = DEFAULT_BATTLE_FORMAT) -> Player | str:
31
+ """
32
+ Creates and initializes a *single* agent instance with a unique username.
33
+ Returns the Player object on success, or an error string on failure.
34
+ """
35
+ print(f"Attempting to create agent of type: {agent_type}")
36
+ player: Player | None = None
37
+ error_message: str | None = None
38
+
39
+ # Generate a unique suffix for this instance
40
+ agent_suffix = random.randint(10000, 999999) # Wider range for uniqueness
41
+
42
+ try:
43
+ if agent_type == "Random Player":
44
+ username = f"{RANDOM_PLAYER_BASE_NAME}{agent_suffix}"
45
+ account_config = AccountConfiguration(username, None) # Guest account
46
+ print(f"Initializing RandomPlayer with username: {username}")
47
+ player = RandomPlayer(
48
+ account_configuration=account_config,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  server_configuration=custom_config,
50
+ battle_format=battle_format,
51
+ # Add a start_listening=False initially if needed, manage connection explicitly
52
+ # start_listening=False # Let's try without first
53
  )
54
+ # await player.connect() # Explicit connect might be needed if start_listening=False
55
+
56
+ elif agent_type == "OpenAI Agent":
57
+ # Check for API key early
58
+ if not os.getenv("OPENAI_API_KEY"):
59
+ error_message = "Error: Cannot create OpenAI Agent. OPENAI_API_KEY environment variable is missing."
60
+ print(error_message)
61
+ return error_message # Return early
62
 
63
+ username = f"{OPENAI_AGENT_BASE_NAME}{agent_suffix}"
64
+ account_config = AccountConfiguration(username, None) # Guest account
65
+ print(f"Initializing OpenAIAgent with username: {username}")
66
+ player = OpenAIAgent( # Make sure your OpenAIAgent accepts these args
67
+ account_configuration=account_config,
68
  server_configuration=custom_config,
69
+ battle_format=battle_format,
70
+ # start_listening=False
71
  )
72
+ # await player.connect()
73
+
74
+ else:
75
+ error_message = f"Error: Invalid agent type '{agent_type}' requested."
76
+ print(error_message)
77
+ return error_message
78
+
79
+ # A short wait might be necessary for the player to establish connection/login
80
+ # This is often handled implicitly by poke-env's internal loops when
81
+ # start_listening=True (default), but managing explicitly can be complex.
82
+ # Let's rely on send_challenges potentially waiting if needed.
83
+ # await asyncio.sleep(2) # Add small delay only if connection issues arise
84
+
85
+ print(f"Agent ({username}) created successfully (object: {player}).")
86
+ return player # Return the player instance
87
+
88
+ except Exception as e:
89
+ agent_name = username if 'username' in locals() else agent_type
90
+ error_message = f"Error creating agent {agent_name}: {e}"
91
+ print(error_message)
92
+ traceback.print_exc() # Log detailed error
93
+ # Ensure cleanup if partial creation occurred (less likely without start_listening=False)
94
+ # if player and hasattr(player, 'disconnect'):
95
+ # try:
96
+ # await player.disconnect()
97
+ # except Exception as disconnect_e:
98
+ # print(f"Error during cleanup disconnect: {disconnect_e}")
99
+ return error_message # Return the error string
100
+
101
+ # --- Battle Invitation Logic (Remains mostly the same, uses the created player) ---
102
+ async def send_battle_invite_async(player: Player, opponent_username: str, battle_format: str):
103
  """Sends a challenge using the provided player object."""
104
+ # Player should already be created and potentially connected by create_agent_async
105
+ if not isinstance(player, Player):
106
+ # This case should ideally be caught earlier, but adding safety check
107
+ return f"Error: Invalid player object passed to send_battle_invite_async: {player}"
108
+
109
+ player_username = getattr(player, 'username', 'unknown_agent') # Get username safely
110
+
111
  try:
112
+ print(f"Attempting to send challenge from {player_username} to {opponent_username} in format {battle_format}")
113
+ # Ensure the player's connection is ready if not automatically handled.
114
+ # If using start_listening=False, ensure player.connect() was called and awaited.
115
+ # The send_challenges method might handle waiting for login internally.
116
+ await player.send_challenges(opponent_username, n_challenges=1, packed_team=None, battle_format=battle_format) # Specify format if needed
117
+ print(f"Challenge sent successfully from {player_username} to {opponent_username}.")
118
+ return f"Battle invitation ({battle_format}) sent to {opponent_username} from bot {player_username}! Check Showdown."
119
+
120
  except Exception as e:
121
+ print(f"Error sending challenge from {player_username}:")
122
+ traceback.print_exc()
123
+ return f"Error sending challenge from {player_username}: {str(e)}. Check console logs."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ # --- No explicit disconnect here - let the scope manage it ---
126
+ # finally:
127
+ # # Attempt to disconnect the temporary agent after the challenge is sent (or fails)
128
+ # if player and hasattr(player, 'disconnect'):
129
+ # print(f"Disconnecting temporary agent: {player_username}")
130
+ # try:
131
+ # await player.disconnect()
132
+ # except Exception as disconnect_e:
133
+ # print(f"Error disconnecting {player_username}: {disconnect_e}")
134
+
135
+
136
+ # --- Gradio Interface Function (Sync Wrapper) ---
137
+ def invite_to_battle(agent_choice: str, username: str):
138
+ """
139
+ Handles the Gradio button click: Creates an agent, sends invite, and returns status.
140
+ This function is SYNCHRONOUS as required by Gradio's fn handler.
141
+ """
142
  username_clean = username.strip()
143
  if not username_clean:
144
  return "Please enter your Showdown username."
145
+ if not agent_choice:
146
+ return "Please select an agent type."
147
 
148
+ # Define the async tasks to be run for this request
149
+ async def _run_async_tasks(selected_agent_type, target_username):
150
+ # 1. Create the agent for this specific request
151
+ agent_or_error = await create_agent_async(selected_agent_type, DEFAULT_BATTLE_FORMAT)
152
+
153
+ if isinstance(agent_or_error, str): # Check if creation returned an error string
154
+ return agent_or_error # Return the error message from creation
155
+
156
+ # 2. If agent created successfully, send the challenge
157
+ player_instance = agent_or_error
158
+ result = await send_battle_invite_async(player_instance, target_username, DEFAULT_BATTLE_FORMAT)
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # 3. poke-env usually handles cleanup when the player object goes out of scope
161
+ # or the event loop managing it finishes, especially with asyncio.run.
162
+ # If persistent connection issues arise, explicit player.disconnect() might
163
+ # be needed within send_battle_invite_async's finally block or here.
164
+ print(f"Async task for {getattr(player_instance, 'username', 'agent')} completed.")
165
  return result
166
+
167
+ # Run the async tasks within the synchronous Gradio handler
168
+ try:
169
+ # asyncio.run creates a new event loop, runs the coroutine, and closes the loop.
170
+ # This is suitable for managing the short lifecycle of the temporary agent.
171
+ print(f"Starting async task execution for request: {agent_choice} vs {username_clean}")
172
+ result = asyncio.run(_run_async_tasks(agent_choice, username_clean))
173
+ print(f"Async task finished. Result: {result}")
174
+ return result
175
+ except RuntimeError as e:
176
+ # Handle cases where asyncio.run might conflict (e.g., nested loops, rare)
177
+ print(f"RuntimeError during asyncio.run: {e}")
178
+ traceback.print_exc()
179
+ # Check if it's the "cannot run loop while another is running" error
180
+ if "cannot run loop" in str(e):
181
+ return "Error: Could not execute task due to conflicting event loop activity. Please try again."
182
+ else:
183
+ return f"An unexpected runtime error occurred: {e}"
184
  except Exception as e:
185
+ print(f"Unexpected error in invite_to_battle sync wrapper: {e}")
 
186
  traceback.print_exc()
187
+ return f"An critical error occurred: {e}"
188
 
189
 
190
+ # --- Gradio UI Definition (No changes needed here) ---
191
+ # iframe code to embed Pokemon Showdown (using a potentially more stable client link if needed)
192
  iframe_code = """
193
  <iframe
194
+ src="https://play.pokemonshowdown.com/"
195
  width="100%"
196
  height="800"
197
  style="border: none;"
198
  referrerpolicy="no-referrer">
199
  </iframe>
200
  """
201
+ # Note: Changed iframe src to the main client. If your custom server needs a specific
202
+ # client URL like the one you had, change it back. Ensure CORS/embedding is allowed.
203
 
204
  def main_app():
205
  """Creates and returns the Gradio application interface."""
206
+ # NO agent initialization at startup anymore
207
+ # start_agent_initialization() # REMOVED
208
 
 
209
  with gr.Blocks(title="Pokemon Showdown Agent") as demo:
210
  gr.Markdown("# Pokémon Battle Agent")
211
  gr.Markdown(
212
  "Select an agent, enter **your** Showdown username "
213
+ "(the one you are logged in with below), and click Send Invite. "
214
+ "A temporary bot with a unique name will be created for the challenge."
215
  )
216
 
 
217
  with gr.Row():
 
218
  agent_dropdown = gr.Dropdown(
219
  label="Select Agent",
220
  choices=["Random Player", "OpenAI Agent"],
221
  value="Random Player",
222
+ scale=1
223
  )
224
  name_input = gr.Textbox(
225
  label="Your Pokémon Showdown Username",
226
  placeholder="Enter username used in Showdown below",
227
+ scale=2
228
  )
229
+ battle_button = gr.Button("Send Battle Invitation", scale=1)
230
 
231
+ # --- Display area for status/results ---
232
+ status_output = gr.Textbox(label="Status", interactive=False) # Added output field
233
 
 
234
  gr.Markdown("### Pokémon Showdown Interface")
235
  gr.Markdown("Log in/use the username you entered above.")
 
236
  gr.HTML(iframe_code)
237
 
 
238
  battle_button.click(
239
  fn=invite_to_battle,
240
  inputs=[agent_dropdown, name_input],
241
+ outputs=[status_output] # Connect output to the status box
242
  )
243
 
244
  return demo
245
 
246
  # --- Main execution block ---
247
  if __name__ == "__main__":
248
+ # Set OPENAI_API_KEY environment variable if needed, e.g., using python-dotenv
249
+ # from dotenv import load_dotenv
250
+ # load_dotenv()
251
+ # if not os.getenv("OPENAI_API_KEY"):
252
+ # print("Warning: OPENAI_API_KEY not set. OpenAI Agent will not work.")
253
+
254
  app = main_app()
255
+ # Consider server_name/port for accessibility if running locally
256
+ # app.launch(server_name="0.0.0.0", server_port=7860)
257
+ app.launch()