Files changed (1) hide show
  1. app.py +228 -61
app.py CHANGED
@@ -1,63 +1,230 @@
 
 
 
1
  import os
2
- from flask import Flask, render_template_string
3
-
4
- app = Flask(__name__)
5
-
6
- # HTML template using Jinja2 syntax to insert the URL safely
7
- # Added basic styling to make the iframe fill the page
8
- HTML_TEMPLATE = """
9
- <!DOCTYPE html>
10
- <html>
11
- <head>
12
- <title>Iframe Viewer</title>
13
- <style>
14
- html, body {
15
- margin: 0;
16
- padding: 0;
17
- height: 100%;
18
- width: 100%;
19
- overflow: hidden; /* Prevent scrollbars on the main page */
20
- }
21
- iframe {
22
- display: block; /* Removes bottom space under iframe */
23
- width: 100%;
24
- height: 100%;
25
- border: none; /* Remove default border */
26
- }
27
- .error {
28
- padding: 20px;
29
- color: red;
30
- font-family: sans-serif;
31
- }
32
- </style>
33
- </head>
34
- <body>
35
- {% if iframe_url %}
36
- <iframe src="{{ iframe_url }}">
37
- Your browser does not support iframes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </iframe>
39
- {% else %}
40
- <div class="error">
41
- Error: The 'IFRAME_URL' secret is not set in the Space settings.
42
- Please add it under Settings -> Repository secrets.
43
- </div>
44
- {% endif %}
45
- </body>
46
- </html>
47
- """
48
-
49
- @app.route('/')
50
- def display_iframe():
51
- # Read the URL from the environment variable 'IFRAME_URL'
52
- # os.getenv returns None if the variable isn't set
53
- url_from_env = os.getenv('IFRAME_URL')
54
- print(url_from_env)
55
- # Pass the URL to the template rendering function
56
- # Flask's render_template_string automatically handles basic HTML escaping
57
- # for security if used directly in text, but here it's okay for the src attribute.
58
- return render_template_string(HTML_TEMPLATE, iframe_url=url_from_env)
59
-
60
- if __name__ == '__main__':
61
- # Hugging Face Spaces expect the app to run on port 7860
62
- # Binding to '0.0.0.0' makes it accessible from outside the container
63
- app.run(host='0.0.0.0', port=7860)
 
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
9
+
10
+
11
+ from poke_env.player import Player, RandomPlayer
12
+ from poke_env import AccountConfiguration, ServerConfiguration
13
+
14
+ from agents import OpenAIAgent
15
+
16
+ # --- Configuration ---
17
+ POKE_SERVER_URL = "wss://jofthomas.com/showdown/websocket"
18
+ POKE_AUTH_URL = "https://jofthomas.com/showdown/action.php"
19
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s')
20
+
21
+ # --- Constants ---
22
+ RANDOM_PLAYER_BASE_NAME = "RandAgent"
23
+ OPENAI_AGENT_BASE_NAME = "OpenAIAgent"
24
+ DEFAULT_BATTLE_FORMAT = "gen9randombattle"
25
+ custom_config = ServerConfiguration(POKE_SERVER_URL, POKE_AUTH_URL)
26
+
27
+
28
+ # --- Agent Creation (Async - Required by poke-env) ---
29
+ async def create_agent_async(agent_type: str, battle_format: str = DEFAULT_BATTLE_FORMAT) -> Player | str:
30
+ """
31
+ Creates and initializes a SINGLE agent instance with a unique username.
32
+ This function MUST be async because Player initialization involves async network setup.
33
+ Returns the Player object on success, or an error string on failure.
34
+ """
35
+ logging.info(f"Attempting to create agent of type: {agent_type}")
36
+ player: Player | None = None
37
+ error_message: str | None = None
38
+ username: str = "unknown_agent"
39
+
40
+ agent_suffix = random.randint(10000, 999999)
41
+
42
+ try:
43
+ if agent_type == "Random Player":
44
+ username = f"{RANDOM_PLAYER_BASE_NAME}{agent_suffix}"
45
+ account_config = AccountConfiguration(username, None)
46
+ logging.info(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
+ start_listening=True,
52
+ )
53
+ elif agent_type == "OpenAI Agent":
54
+ if not os.getenv("OPENAI_API_KEY"):
55
+ error_message = "Error: Cannot create OpenAI Agent. OPENAI_API_KEY environment variable is missing."
56
+ logging.error(error_message)
57
+ return error_message
58
+ username = f"{OPENAI_AGENT_BASE_NAME}{agent_suffix}"
59
+ account_config = AccountConfiguration(username, None)
60
+ logging.info(f"Initializing OpenAIAgent with username: {username}")
61
+ player = OpenAIAgent(
62
+ account_configuration=account_config,
63
+ server_configuration=custom_config,
64
+ battle_format=battle_format,
65
+ start_listening=True,
66
+ )
67
+ else:
68
+ error_message = f"Error: Invalid agent type '{agent_type}' requested."
69
+ logging.error(error_message)
70
+ return error_message
71
+
72
+ logging.info(f"Agent object ({username}) created successfully.")
73
+ return player
74
+
75
+ except Exception as e:
76
+ error_message = f"Error creating agent {username}: {e}"
77
+ logging.error(error_message)
78
+ logging.error(traceback.format_exc())
79
+ return error_message
80
+
81
+ # --- Battle Invitation (Async - Required by poke-env) ---
82
+ async def send_battle_invite_async(player: Player, opponent_username: str, battle_format: str) -> str:
83
+ """
84
+ Sends a challenge using the provided player object.
85
+ This function MUST be async as sending challenges involves network I/O.
86
+ Returns a status string (success or error message).
87
+ """
88
+ if not isinstance(player, Player):
89
+ err_msg = f"Internal Error: Invalid object passed instead of Player: {type(player)}"
90
+ logging.error(err_msg)
91
+ # In background thread, we might just log this and exit thread
92
+ raise TypeError(err_msg) # Raise exception to be caught by the thread runner
93
+
94
+ player_username = getattr(player, 'username', 'unknown_agent')
95
+
96
+ try:
97
+ logging.info(f"Attempting to send challenge from {player_username} to {opponent_username} in format {battle_format}")
98
+ await player.send_challenges(opponent_username, n_challenges=1)
99
+ success_msg = f"Battle invitation ({battle_format}) sent to '{opponent_username}' from bot '{player_username}'."
100
+ logging.info(success_msg)
101
+ return success_msg # Indicate success
102
+
103
+ except Exception as e:
104
+ error_msg = f"Error sending challenge from {player_username} to {opponent_username}: {e}"
105
+ logging.error(error_msg)
106
+ logging.error(traceback.format_exc())
107
+ # Re-raise or return error indication for the thread runner
108
+ raise e # Raise exception to be caught by the thread runner
109
+
110
+
111
+ # --- Background Task Runner (Runs in a separate thread) ---
112
+ def run_invite_in_background(agent_choice: str, target_username: str, battle_format: str):
113
+ """
114
+ This function runs in a separate thread for each invite request.
115
+ It sets up and runs the asyncio operations needed for one invite.
116
+ """
117
+ thread_name = threading.current_thread().name
118
+ logging.info(f"Background thread '{thread_name}' started for {agent_choice} vs {target_username}.")
119
+
120
+ async def _run_async_challenge_steps():
121
+ """The async steps to be run via asyncio.run() in this thread."""
122
+ agent_or_error = await create_agent_async(agent_choice, battle_format)
123
+
124
+ if isinstance(agent_or_error, str):
125
+ # Agent creation failed, log the error message from create_agent_async
126
+ logging.error(f"[{thread_name}] Agent creation failed: {agent_or_error}")
127
+ # No further action needed in this thread
128
+ return
129
+
130
+ player_instance: Player = agent_or_error
131
+ player_username = getattr(player_instance, 'username', 'agent')
132
+ logging.info(f"[{thread_name}] Agent {player_username} created, proceeding to challenge {target_username}.")
133
+
134
+ try:
135
+ result = await send_battle_invite_async(player_instance, target_username, battle_format)
136
+ # Log the success message from send_battle_invite_async
137
+ logging.info(f"[{thread_name}] Challenge result: {result}")
138
+ except Exception as invite_error:
139
+ # Log errors from send_battle_invite_async
140
+ # Error message/traceback already logged inside send_battle_invite_async
141
+ logging.error(f"[{thread_name}] Failed to send challenge from {player_username} to {target_username}. Error: {invite_error}")
142
+ finally:
143
+ pass
144
+
145
+ try:
146
+ asyncio.run(_run_async_challenge_steps())
147
+ logging.info(f"Background thread '{thread_name}' finished successfully for {target_username}.")
148
+ except RuntimeError as e:
149
+ logging.error(f"[{thread_name}] asyncio RuntimeError: {e}")
150
+ logging.error(traceback.format_exc())
151
+ except Exception as e:
152
+ logging.error(f"[{thread_name}] Unexpected error in background task: {e}")
153
+ logging.error(traceback.format_exc())
154
+
155
+ # --- Gradio Interface Logic (Starts the background thread) ---
156
+ def start_invite_thread(agent_choice: str, username: str) -> str:
157
+ """
158
+ Handles the Gradio button click (Synchronous, but FAST).
159
+ Performs basic validation and starts a background thread to handle
160
+ the actual agent creation and invitation process.
161
+ Returns an immediate status message to Gradio.
162
+ """
163
+ username_clean = username.strip()
164
+ if not username_clean:
165
+ return "⚠️ Please enter your Showdown username."
166
+ if not agent_choice:
167
+ return "⚠️ Please select an agent type."
168
+
169
+ logging.info(f"Received request: Agent={agent_choice}, Opponent={username_clean}. Starting background thread.")
170
+
171
+ # Create and start the background thread
172
+ thread = threading.Thread(
173
+ target=run_invite_in_background,
174
+ args=(agent_choice, username_clean, DEFAULT_BATTLE_FORMAT),
175
+ daemon=True # Set as daemon so threads don't block app exit
176
+ )
177
+ thread.start()
178
+
179
+ # Return immediately to Gradio UI
180
+ return f"✅ Invite process for '{username_clean}' started in background. Check Pokémon Showdown and logs for status."
181
+
182
+
183
+ # --- Gradio UI Definition ---
184
+ # [ main_app function remains the same, but the button click now calls start_invite_thread ]
185
+ def main_app():
186
+ """Creates and returns the Gradio application interface."""
187
+
188
+ agent_options = ["Random Player"]
189
+ agent_options.append("OpenAI Agent")
190
+
191
+ # Use a more descriptive title if possible
192
+ with gr.Blocks(title="Pokemon Showdown Multi-Challenger") as demo:
193
+ gr.Markdown("# Pokémon Battle Agent Challenger")
194
+ gr.Markdown(
195
+ "1. Choose a name in the Iframe, if you have an account, you can also connect.\n"
196
+ "2. Select an agent type.\n"
197
+ "3. Enter **your** Showdown username (the one you are logged in with below).\n"
198
+ "4. Click 'Send Battle Invitation'. You can click multiple times for different users.\n\n"
199
+ "A temporary bot will be created *in the background* to send the challenge in `gen9randombattle` format."
200
+ )
201
+
202
+ with gr.Row():
203
+ agent_dropdown = gr.Dropdown(
204
+ label="Select Agent", choices=agent_options, value=agent_options[0], scale=1
205
+ )
206
+ name_input = gr.Textbox(
207
+ label="Your Pokémon Showdown Username", placeholder="Enter username used in Showdown below", scale=2
208
+ )
209
+ #variant="primary"
210
+ battle_button = gr.Button("Send Battle Invitation", scale=1)
211
+ gr.HTML("""
212
+ <iframe
213
+ src="https://jofthomas.com/play.pokemonshowdown.com/testclient.html"
214
+ width="100%" height="800" style="border: none;" referrerpolicy="no-referrer">
215
  </iframe>
216
+ """)
217
+
218
+ # *** IMPORTANT: Update the click handler ***
219
+ battle_button.click(
220
+ fn=start_invite_thread, # Calls the function that starts the thread
221
+ inputs=[agent_dropdown, name_input],
222
+ )
223
+
224
+ return demo
225
+
226
+ # --- Application Entry Point ---
227
+ # [ if __name__ == "__main__": block remains the same ]
228
+ if __name__ == "__main__":
229
+ app = main_app()
230
+ app.launch()