Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,537 +1,233 @@
|
|
1 |
-
#
|
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
|
|
|
|
|
13 |
|
14 |
-
# ---
|
15 |
-
|
|
|
16 |
from poke_env import AccountConfiguration, ServerConfiguration
|
17 |
-
|
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 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
43 |
DEFAULT_BATTLE_FORMAT = "gen9randombattle"
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
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 |
-
|
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 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
return True # Indicate success
|
190 |
|
191 |
except Exception as e:
|
192 |
-
error_msg = f"
|
193 |
-
|
194 |
-
|
195 |
-
#
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
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 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
#
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
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 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
except asyncio.CancelledError:
|
402 |
-
# Logged above
|
403 |
-
pass
|
404 |
except Exception as e:
|
405 |
-
|
406 |
-
traceback.
|
407 |
-
|
408 |
-
|
409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
|
|
|
|
|
|
|
411 |
def main_app():
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
#
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
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 |
-
# ---
|
|
|
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 |
-
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|