44import random
55import re
66from datetime import datetime
7- from typing import Any
7+ from typing import Any , List , Dict , Tuple
88import aiohttp
99import numpy as np
1010import requests
1111import asyncio
1212from amongagents .agent .neutral_prompts import *
1313
14+ # Global dictionary to store futures for human actions, keyed by game_id
15+ human_action_futures : Dict [int , asyncio .Future ] = {}
16+
1417class Agent :
1518 def __init__ (self , player ):
1619 self .player = player
@@ -245,7 +248,11 @@ async def choose_action(self, timestep):
245248 return action
246249
247250 def choose_observation_location (self , map ):
248- return random .sample (map , 1 )[0 ]
251+ if isinstance (map , (list , tuple )):
252+ return random .choice (map )
253+ else :
254+ # For sets, dicts, or other non-sequence types
255+ return random .choice (list (map ))
249256
250257
251258class RandomAgent (Agent ):
@@ -272,69 +279,195 @@ def __init__(self, player, tools=None, game_index=0, agent_config=None, list_of_
272279 self .game_index = game_index
273280 self .summarization = "No thought process has been made."
274281 self .processed_memory = "No memory has been processed."
275- self .log_path = os .getenv ("EXPERIMENT_PATH" ) + "/agent-logs.json"
276- self .compact_log_path = os .getenv ("EXPERIMENT_PATH" ) + "/agent-logs-compact.json"
277-
278- # Initialize global session state if it doesn't exist
279- if os .getenv ("FLASK" ) == "True" :
280- # Global session state for actions across page refreshes
281- if "human_actions" not in st .session_state :
282- st .session_state .human_actions = {}
283- if "action_history" not in st .session_state :
284- st .session_state .action_history = {}
285- if "game_started" not in st .session_state :
286- st .session_state .game_started = False
282+ self .log_path = os .getenv ("EXPERIMENT_PATH" , "." ) + "/agent-logs.json"
283+ self .compact_log_path = os .getenv ("EXPERIMENT_PATH" , "." ) + "/agent-logs-compact.json"
284+ self .current_available_actions : List [Any ] = []
285+ self .current_step = 0
286+ self .max_steps = 50 # Default value, will be updated from game config
287+ self .action_future = None # Store the future as an instance variable
287288
288- async def choose_action (self , timestep ):
289+ def update_max_steps (self , max_steps ):
290+ """Update the max_steps value from the game config."""
291+ self .max_steps = max_steps
292+
293+ async def choose_action (self , timestep : int ):
294+ """
295+ Chooses an action, either via web interface (if FLASK_ENABLED=True)
296+ or command line (if FLASK_ENABLED=False).
297+ """
298+ use_flask = os .getenv ("FLASK_ENABLED" , "True" ) == "True"
289299 all_info = self .player .all_info_prompt ()
290- available_actions = self .player .get_available_actions ()
291-
292- # Log the start of action selection
293- action_prompt = f"Available actions:\n " + "\n " .join ([f"{ i + 1 } : { action } " for i , action in enumerate (available_actions )])
294- full_prompt = {
295- "All Info" : all_info ,
296- "Available Actions" : action_prompt
297- }
298- # Command line interface
299- print (f"{ str (self .player )} " )
300- print (all_info )
301- print ("Choose an action:" )
302- for i , action in enumerate (available_actions ):
303- print (f"{ i + 1 } : { action } " )
300+ self .current_available_actions = self .player .get_available_actions ()
301+ self .current_step = timestep
302+
303+ if use_flask :
304+ # --- Web Interface Logic ---
305+ action_prompt = "Waiting for human action via web interface.\n Available actions:\n " + "\n " .join ([f"{ i + 1 } : { str (action )} " for i , action in enumerate (self .current_available_actions )])
306+ full_prompt = {
307+ "All Info" : all_info ,
308+ "Available Actions" : action_prompt ,
309+ "Current Step" : f"{ timestep } /{ self .max_steps } " ,
310+ "Current Player" : self .player .name
311+ }
312+
313+ loop = asyncio .get_event_loop ()
314+ self .action_future = loop .create_future () # Store in instance variable
304315
305- stop_triggered = False
306- valid_input = False
307- while (not stop_triggered ) and (not valid_input ):
316+ # Use game_id from the server instead of game_index
317+ # The game_id is passed to the HumanAgent when it's created
318+ game_id = getattr (self , 'game_id' , self .game_index )
319+ human_action_futures [game_id ] = self .action_future
320+
321+ print (f"[Agent] Created future for game { game_id } " )
322+ print (f"[Agent] Available futures: { list (human_action_futures .keys ())} " )
323+
324+ print (f"\n [Game { game_id } ] Human player { self .player .name } 's turn. Waiting for action via web interface..." )
325+ print (f"Available actions: { [str (a ) for a in self .current_available_actions ]} " )
326+
308327 try :
309- action_idx = int (input ())
310- if action_idx == 0 :
311- stop_triggered = True
312- elif action_idx < 1 or action_idx > len (available_actions ):
313- raise ValueError (f"Invalid input. Please enter a number between 1 and { len (available_actions )} ." )
328+ chosen_action_data = await self .action_future
329+ action_idx = chosen_action_data .get ("action_index" )
330+ action_message = chosen_action_data .get ("message" )
331+
332+ if action_idx is None or action_idx < 0 or action_idx >= len (self .current_available_actions ):
333+ print (f"[Game { game_id } ] Invalid action index received: { action_idx } . Defaulting to first action." )
334+ selected_action = self .current_available_actions [0 ]
314335 else :
315- valid_input = True
316- except :
317- print ("Invalid input. Please enter a number." )
318- continue
336+ selected_action = self .current_available_actions [action_idx ]
337+
338+ response_log = f"[Action] { str (selected_action )} "
339+ # Check if action requires a message (e.g., SPEAK)
340+ # Use str() and check for attributes robustly
341+ is_speak_action = False
342+ if hasattr (selected_action , 'name' ): # Check attribute exists
343+ is_speak_action = selected_action .name == "SPEAK"
344+ elif "SPEAK" in str (selected_action ): # Fallback to string check
345+ is_speak_action = True
346+
347+ if is_speak_action and action_message :
348+ if hasattr (selected_action , 'provide_message' ):
349+ selected_action .provide_message (action_message )
350+ elif hasattr (selected_action , 'message' ): # Fallback to setting attribute
351+ selected_action .message = action_message
352+ response_log += f" with message: { action_message } "
353+
354+ self .log_interaction (sysprompt = "Human Agent (Web)" , prompt = full_prompt ,
355+ original_response = response_log ,
356+ step = timestep )
357+
358+ # Clear the future and actions only after successful action selection
359+ if game_id in human_action_futures :
360+ print (f"[Agent] Deleting future for game { game_id } after successful action" )
361+ del human_action_futures [game_id ]
362+ self .current_available_actions = []
363+ self .action_future = None
364+
365+ return selected_action
366+
367+ except asyncio .CancelledError :
368+ print (f"[Game { game_id } ] Human action cancelled." )
369+ # Clean up on cancellation
370+ if game_id in human_action_futures :
371+ print (f"[Agent] Deleting future for game { game_id } after cancellation" )
372+ del human_action_futures [game_id ]
373+ self .current_available_actions = []
374+ self .action_future = None
375+ raise
376+ else :
377+ # --- Command Line Interface Logic ---
378+ action_prompt = "Available actions:\n " + "\n " .join ([f"{ i + 1 } : { str (action )} " for i , action in enumerate (self .current_available_actions )])
379+ full_prompt = {
380+ "All Info" : all_info ,
381+ "Available Actions" : action_prompt
382+ }
383+
384+ print (f"\n --- [Game { self .game_index } ] Player: { self .player .name } ({ self .player .identity if self .player .identity else 'Role Unknown' } ) ---" )
385+ print (all_info )
386+ print ("\n Choose an action:" )
387+ for i , action in enumerate (self .current_available_actions ):
388+ print (f"{ i + 1 } : { str (action )} " )
389+ print ("(Enter 0 to stop game)" )
390+
391+ stop_triggered = False
392+ valid_input = False
393+ selected_action = None
394+ action_idx_chosen = - 1
395+
396+ while (not stop_triggered ) and (not valid_input ):
397+ try :
398+ user_input = input ("> " )
399+ action_idx_chosen = int (user_input )
400+ if action_idx_chosen == 0 :
401+ stop_triggered = True
402+ elif action_idx_chosen < 1 or action_idx_chosen > len (self .current_available_actions ):
403+ print (f"Invalid input. Please enter a number between 1 and { len (self .current_available_actions )} (or 0 to stop)." )
404+ else :
405+ valid_input = True
406+ except ValueError :
407+ print ("Invalid input. Please enter a number." )
408+ continue
409+
410+ if stop_triggered :
411+ print ("Stopping game as requested by user." )
412+ # How to signal stop? Raise exception? Return specific value?
413+ # For now, raise an exception that the game loop might catch.
414+ raise KeyboardInterrupt ("Game stopped by user via CLI." )
319415
320- if stop_triggered :
321- raise ValueError ("Game stopped by user." )
416+ selected_action = self .current_available_actions [action_idx_chosen - 1 ]
417+ response_log = f"[Action] { str (selected_action )} "
418+
419+ # Check if action requires a message using string check
420+ is_speak_action = False
421+ if hasattr (selected_action , 'name' ):
422+ is_speak_action = selected_action .name == "SPEAK"
423+ elif "SPEAK" in str (selected_action ):
424+ is_speak_action = True
425+
426+ if is_speak_action :
427+ print ("Enter your message:" )
428+ action_message = input ("> " )
429+ if hasattr (selected_action , 'provide_message' ):
430+ selected_action .provide_message (action_message )
431+ elif hasattr (selected_action , 'message' ):
432+ selected_action .message = action_message
433+ else :
434+ print ("Warning: Could not set message for SPEAK action." )
435+ response_log += f" with message: { action_message } "
322436
323- selected_action = available_actions [action_idx - 1 ]
437+ self .log_interaction (sysprompt = "Human Agent (CLI)" , prompt = full_prompt ,
438+ original_response = response_log ,
439+ step = timestep )
324440
325- if selected_action .name == "SPEAK" :
326- print ("Enter your response:" )
327- action_message = input ()
328- selected_action .provide_message (action_message )
329- self .log_interaction (sysprompt = "Human Agent" , prompt = full_prompt ,
330- original_response = f"[Action] { selected_action } with message: { action_message } " ,
331- step = timestep )
332- else :
333- self .log_interaction (sysprompt = "Human Agent" , prompt = full_prompt ,
334- original_response = f"[Action] { selected_action } " ,
335- step = timestep )
336-
337- return selected_action
441+ self .current_available_actions = [] # Clear actions after use
442+ return selected_action # Return synchronously within async def
443+
444+ def get_current_state_for_web (self ) -> Dict [str , Any ]:
445+ """
446+ Returns the necessary state for the web UI when it's the human's turn.
447+ Uses string checks for action properties.
448+ """
449+ available_actions_web = []
450+ for action in self .current_available_actions :
451+ action_str = str (action )
452+ requires_message = False
453+ if hasattr (action , 'name' ):
454+ requires_message = action .name == "SPEAK"
455+ elif "SPEAK" in action_str :
456+ requires_message = True
457+
458+ available_actions_web .append ({
459+ "name" : action_str ,
460+ "requires_message" : requires_message
461+ })
462+
463+ return {
464+ "is_human_turn" : True ,
465+ "player_name" : self .player .name ,
466+ "player_info" : self .player .all_info_prompt (),
467+ "available_actions" : available_actions_web ,
468+ "current_step" : f"{ self .current_step } /{ self .max_steps } " ,
469+ "current_player" : self .player .name
470+ }
338471
339472 def respond (self , message ):
340473 print (message )
@@ -355,6 +488,11 @@ def choose_observation_location(self, map):
355488
356489 def log_interaction (self , sysprompt , prompt , original_response , step ):
357490 """Log human player interactions similar to LLMAgent"""
491+ # Ensure log path exists
492+ log_dir = os .path .dirname (self .log_path )
493+ if log_dir and not os .path .exists (log_dir ):
494+ os .makedirs (log_dir , exist_ok = True )
495+
358496 interaction = {
359497 'game_index' : 'Game ' + str (self .game_index ),
360498 'step' : step ,
0 commit comments