| import gradio as gr |
|
|
| from support.api_keys_manager import get_required_teams |
| from support.build_graph import MyGraph |
| from support.database import save_game_to_db |
| from support.log_manager import logger |
| from support.manage_game import Game |
| from support.game_settings import TEAM_MODEL_PRESETS, JS_BTN |
| from support.utils import format_messages_as_feed, generate_team_html, plot_game_board_with_guesses |
|
|
|
|
| |
| TEAM_OPTIONS = list(TEAM_MODEL_PRESETS.keys()) + ["random"] |
|
|
|
|
| def validate_api_keys(red_team, blue_team, openai_key, google_key, anthropic_key, hf_key, current_api_keys): |
| """Validate that all required API keys are provided.""" |
| required_teams = get_required_teams(red_team, blue_team) |
|
|
| |
| api_keys = {} |
| missing_keys = [] |
|
|
| key_mapping = { |
| "openai": (openai_key, "OPENAI_API_KEY", "OpenAI"), |
| "google": (google_key, "GOOGLE_API_KEY", "Google"), |
| "anthropic": (anthropic_key, "ANTHROPIC_API_KEY", "Anthropic"), |
| "opensource": (hf_key, "HUGGINGFACEHUB_API_TOKEN", "HuggingFace") |
| } |
|
|
| for team in required_teams: |
| key_value, key_name, display_name = key_mapping[team] |
| if key_value and key_value.strip(): |
| api_keys[key_name] = key_value.strip() |
| else: |
| missing_keys.append(display_name) |
|
|
| if missing_keys: |
| error_msg = f"⚠️ Missing API keys for: {', '.join(missing_keys)}" |
| return api_keys, False, error_msg |
|
|
| return api_keys, True, "✅ All API keys validated!" |
|
|
|
|
| with gr.Blocks(fill_width=True) as demo: |
| game_state = gr.State() |
| players_state = gr.State(value=[]) |
| board_state = gr.State(value={}) |
| game_started = gr.State(value=False) |
| guessed_words_state = gr.State(value=[]) |
| messages_state = gr.State(value=[]) |
| chat_history_state = gr.State(value=[]) |
| is_human_playing_state = gr.State(value=False) |
| turn_state = gr.State(value=0) |
| winners_state = gr.State(value=None) |
| api_keys_state = gr.State(value={}) |
| graph_instance = gr.State() |
|
|
| |
| with gr.Row(elem_id="game_setup_row"): |
| with gr.Row(elem_id="team_selection_row") as team_selection: |
| red_team_dropdown = gr.Dropdown( |
| choices=TEAM_OPTIONS, |
| value="google", |
| label="🔴 Red Team", |
| info="Select the model team for the Red side.", |
| elem_id="red_team_dropdown", |
| ) |
| blue_team_dropdown = gr.Dropdown( |
| choices=TEAM_OPTIONS, |
| value="openai", |
| label="🔵 Blue Team", |
| info="Select the model team for the Blue side.", |
| elem_id="blue_team_dropdown" |
| ) |
|
|
| with gr.Group(visible=True, elem_id="api_keys_section") as api_keys_section: |
| gr.Markdown("### 🔑 API Keys") |
| gr.Markdown("*Enter API keys for the selected teams. Keys are only required for active teams.*") |
|
|
| google_api_input = gr.Textbox( |
| label="Google API Key", |
| placeholder="AIza...", |
| type="password", |
| visible=True, |
| elem_id="google_api_key", |
| interactive=True |
| ) |
|
|
| openai_api_input = gr.Textbox( |
| label="OpenAI API Key", |
| placeholder="sk-...", |
| type="password", |
| visible=True, |
| elem_id="openai_api_key", |
| interactive=True |
| ) |
|
|
| anthropic_api_input = gr.Textbox( |
| label="Anthropic API Key", |
| placeholder="sk-ant-...", |
| type="password", |
| visible=False, |
| elem_id="anthropic_api_key", |
| interactive=True |
| ) |
|
|
| hf_api_input = gr.Textbox( |
| label="HuggingFace API Token", |
| placeholder="hf_...", |
| type="password", |
| visible=False, |
| elem_id="hf_api_key" |
| ) |
|
|
| validation_status = gr.Markdown("", visible=False) |
|
|
| new_game_btn = gr.Button("🔄 Generate New Game", variant="secondary", size="sm", elem_id="new_game_btn", visible=False) |
|
|
| start_btn = gr.Button( |
| "🎲 Generate Teams and Start Playing!", |
| variant="primary", |
| size="lg", |
| elem_id="start_game_btn", |
| visible=True |
| ) |
|
|
| |
| with gr.Column(visible=False, elem_id="game_content") as game_content: |
| with gr.Accordion("👥 Teams Overview", open=True, elem_id="teams_accordion") as teams_section: |
| team_html = gr.HTML("") |
| player_info_md = gr.Markdown(value="", elem_id="player_info") |
|
|
| |
| with gr.Row(elem_id="boss_control_row") as play_as_boss_section: |
| with gr.Column(scale=1): |
| gr.Markdown("### 🎮 Play as Boss") |
| with gr.Row(): |
| red_boss_btn = gr.Button("🔴 Play as Red Boss", size="sm", elem_id="red_boss_btn") |
| blue_boss_btn = gr.Button("🔵 Play as Blue Boss", size="sm", elem_id="blue_boss_btn") |
|
|
| |
| with gr.Column(visible=False, elem_id="red_boss_input") as red_boss_input_section: |
| gr.Markdown("#### 👤 Enter Your Name (Red Team)") |
| red_boss_name_input = gr.Textbox( |
| placeholder="Your name...", |
| label="Name", |
| show_label=False, |
| elem_id="red_boss_name_input" |
| ) |
| with gr.Row(elem_id="red_boss_actions"): |
| save_red_boss_btn = gr.Button("💾 Save", variant="primary", size="sm", elem_id="save_red_boss_btn") |
| cancel_red_boss_btn = gr.Button("❌ Cancel", size="sm", elem_id="cancel_red_boss_btn") |
|
|
| with gr.Row(elem_id="red_error"): |
| empty_red = gr.Button("❌ You have to insert a valid name, or cancel", variant="primary", size="sm", visible=False, interactive=False, elem_id="error_red_display") |
|
|
| with gr.Column(visible=False, elem_id="blue_boss_input") as blue_boss_input_section: |
| gr.Markdown("#### 👤 Enter Your Name (Blue Team)") |
| blue_boss_name_input = gr.Textbox( |
| placeholder="Your name...", |
| label="Name", |
| show_label=False, |
| elem_id="blue_boss_name_input" |
| ) |
| with gr.Row(elem_id="blue_boss_actions"): |
| save_blue_boss_btn = gr.Button("💾 Save", variant="primary", size="sm", elem_id="save_blue_boss_btn") |
| cancel_blue_boss_btn = gr.Button("❌ Cancel", size="sm", elem_id="cancel_blue_boss_btn") |
|
|
| with gr.Row(elem_id="blue_error"): |
| empty_blue = gr.Button("❌ You have to insert a valid name, or cancel", variant="primary", size="sm", visible=False, interactive=False, elem_id="error_blue_display") |
|
|
| with gr.Row(elem_id="game_row", equal_height=True): |
| with gr.Column(elem_id="board_column"): |
| gr.Markdown("### 🎯 Game Board", elem_id="board_header") |
| board_plot = gr.Image( |
| label="Game Board", |
| show_label=False, |
| elem_id="game_board_img", |
| ) |
|
|
| with gr.Column(elem_id="chat_column"): |
| gr.Markdown("### 💬 Game Feed", elem_id="chat_header") |
|
|
| |
| message_feed = gr.HTML(value="", elem_id="message_feed_container", autoscroll=True) |
|
|
| with gr.Row(elem_id="input_game_section"): |
| with gr.Row(visible=False) as input_game_values: |
| user_input = gr.Textbox( |
| placeholder="Type your message...", |
| show_label=True, |
| scale=9, |
| visible=True, |
| label="Your Clue" |
| ) |
| dropdown = gr.Dropdown( |
| choices=[i for i in range(1, 10)], |
| label="Clue number", |
| value=1, |
| interactive=True |
| ) |
| send_btn = gr.Button("Send", elem_id="send_message_btn") |
|
|
| |
| def start_game(red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key): |
| |
| api_keys, is_valid, message = validate_api_keys( |
| red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, {} |
| ) |
|
|
| if not is_valid: |
| |
| return { |
| validation_status: gr.update(visible=True, value=message), |
| api_keys_state: api_keys, |
| game_state: None, |
| new_game_btn: gr.update(visible=False), |
| start_btn: gr.update(visible=True), |
| game_content: gr.update(visible=False), |
| team_html: "", |
| board_plot: None, |
| game_started: False, |
| players_state: [], |
| board_state: {}, |
| messages_state: [], |
| graph_instance: None |
| } |
|
|
| |
| game = Game(red_team=red_team_choice, blue_team=blue_team_choice, api_keys=api_keys) |
| starting_team = game.board.get('starting_team', 'red') |
| html = generate_team_html(game.players, starting_team) |
| board_img = plot_game_board_with_guesses(game.board) |
|
|
| |
| graph = MyGraph() |
|
|
| return { |
| game_state: game, |
| new_game_btn: gr.update(visible=True), |
| start_btn: gr.update(visible=False), |
| game_content: gr.update(visible=True), |
| team_html: html, |
| board_plot: board_img, |
| game_started: True, |
| players_state: game.players, |
| board_state: game.board, |
| messages_state: [], |
| api_keys_state: api_keys, |
| validation_status: gr.update(visible=True, value="✅ Game started successfully!"), |
| graph_instance: graph |
| } |
|
|
| def generate_new_game(red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, current_api_keys): |
| |
| api_keys, is_valid, message = validate_api_keys( |
| red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, current_api_keys |
| ) |
|
|
| if not is_valid: |
| |
| raise gr.Error(message) |
|
|
| |
| game = Game(red_team=red_team_choice, blue_team=blue_team_choice, api_keys=api_keys) |
| starting_team = game.board.get('starting_team', 'red') |
| html = generate_team_html(game.players, starting_team) |
| board_img = plot_game_board_with_guesses(game.board) |
|
|
| |
| graph = MyGraph() |
|
|
| return game, html, board_img, game.players, game.board, [], gr.update(visible=True), api_keys, graph |
|
|
| def show_red_boss_input(): |
| return gr.Column(visible=True) |
|
|
| def show_blue_boss_input(): |
| return gr.Column(visible=True) |
|
|
| def hide_red_boss_input(): |
| return gr.Column(visible=False) |
|
|
| def hide_blue_boss_input(): |
| return gr.Column(visible=False) |
|
|
| def update_api_key_inputs(red_team, blue_team): |
| """Update visibility and state of API key inputs based on team selection.""" |
|
|
| required_teams = get_required_teams(red_team, blue_team) |
|
|
| |
| updates = {} |
| for team in ["openai", "google", "anthropic", "opensource"]: |
| updates[team] = gr.update(visible=team in required_teams) |
|
|
| return ( |
| updates["openai"], |
| updates["google"], |
| updates["anthropic"], |
| updates["opensource"] |
| ) |
|
|
| def update_boss_player(team_color, name, players, board, is_human_playing): |
| """Update the boss player with human name and model""" |
| if not name or not name.strip(): |
| return generate_team_html(players, board.get('starting_team')), players, gr.Column(visible=True), gr.Row(visible=True), gr.Row(visible=False), is_human_playing, gr.Button(visible=True) |
|
|
| for player in players: |
| if player.team == team_color and player.role == "boss": |
| player.name = name.strip() |
| player.model_name = "Human brain" |
| break |
|
|
| |
| starting_team = board.get('starting_team') |
| html = generate_team_html(players, starting_team, True) |
| is_human_playing = True |
| return html, players, gr.Column(visible=False), gr.Row(visible=False), gr.Row(visible=True), is_human_playing, gr.Button(visible=False) |
|
|
| async def process_message(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn, graph): |
| """Process user message and stream responses""" |
|
|
| accumulated_messages = list(messages) if messages else [] |
| async for new_msg, guessed, updated_board, updated_chat_history, winners in graph.stream_graph(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn): |
|
|
| if len(new_msg) > len(accumulated_messages): |
| |
| accumulated_messages = list(new_msg) |
| else: |
| |
| accumulated_messages = list(new_msg) |
|
|
| messages = accumulated_messages |
| feed_html = format_messages_as_feed(accumulated_messages, players, winners) |
| yield feed_html, guessed, messages, updated_board, updated_chat_history, winners |
|
|
| def update_plot(guessed_words, board, game_state): |
| |
| if game_state is None: |
| return gr.update() |
|
|
| if not guessed_words or not board: |
| return gr.update() |
|
|
| logger.info(f"Calling update_plot: {guessed_words}") |
| board_img = plot_game_board_with_guesses(board, guessed_words) |
| return board_img |
|
|
| def deactivate_send(game, winner_and_score): |
| if winner_and_score is None or game is None: |
| return gr.Button(visible=True) |
| else: |
| return gr.Button(visible=False) |
|
|
| def send_to_database(game, winner_and_score): |
|
|
| """Save game results to database""" |
| if winner_and_score is None or game is None: |
| return |
|
|
| winner_team = winner_and_score[0] |
| scores = winner_and_score[1] |
| reason = winner_and_score[2] if len(winner_and_score) > 2 else None |
|
|
| |
| logger.info(f"\n{'='*50}") |
| logger.info("GAME COMPLETED - Saving to Database") |
| logger.info(f"{'='*50}") |
| logger.info(f"Winner: {winner_team.upper()} team") |
| logger.info(f"Scores - Red: {scores[0]}, Blue: {scores[1]}") |
| if reason: |
| logger.info(f"Reason: {reason}") |
| logger.info("\nTeam Compositions:") |
| logger.info(f"Red Team ({game.red_team_choice}):") |
| for player in [p for p in game.players if p.team == 'red']: |
| logger.info(f" - {player.role}: {player.name} ({player.model_name})") |
| logger.info(f"Blue Team ({game.blue_team_choice}):") |
| for player in [p for p in game.players if p.team == 'blue']: |
| logger.info(f" - {player.role}: {player.name} ({player.model_name})") |
|
|
| |
| try: |
| game_id = save_game_to_db(game, winner_and_score) |
| logger.info(f"\n✅ Game saved successfully! (ID: {game_id})") |
| logger.info(f"{'='*50}\n") |
| except Exception as e: |
| logger.info(f"\n❌ Error saving game to database: {e}") |
| logger.info(f"{'='*50}\n") |
|
|
|
|
| |
| start_btn.click( |
| fn=start_game, |
| inputs=[red_team_dropdown, blue_team_dropdown, openai_api_input, google_api_input, anthropic_api_input, hf_api_input], |
| outputs=[game_state, new_game_btn, start_btn, game_content, team_html, board_plot, game_started, players_state, board_state, messages_state, api_keys_state, validation_status, graph_instance] |
| ) |
|
|
| |
| new_game_btn.click( |
| fn=generate_new_game, |
| inputs=[ |
| red_team_dropdown, |
| blue_team_dropdown, |
| openai_api_input, |
| google_api_input, |
| anthropic_api_input, |
| hf_api_input, |
| api_keys_state |
| ], |
| outputs=[ |
| game_state, |
| team_html, |
| board_plot, |
| players_state, |
| board_state, |
| messages_state, |
| play_as_boss_section, |
| api_keys_state, |
| graph_instance |
| ] |
| ) |
|
|
| red_team_dropdown.change( |
| fn=update_api_key_inputs, |
| inputs=[red_team_dropdown, blue_team_dropdown], |
| outputs=[openai_api_input, google_api_input, anthropic_api_input, hf_api_input] |
| ) |
|
|
| blue_team_dropdown.change( |
| fn=update_api_key_inputs, |
| inputs=[red_team_dropdown, blue_team_dropdown], |
| outputs=[openai_api_input, google_api_input, anthropic_api_input, hf_api_input] |
| ) |
|
|
| |
| red_boss_btn.click( |
| fn=show_red_boss_input, |
| outputs=[red_boss_input_section] |
| ) |
|
|
| blue_boss_btn.click( |
| fn=show_blue_boss_input, |
| outputs=[blue_boss_input_section] |
| ) |
|
|
| cancel_red_boss_btn.click( |
| fn=hide_red_boss_input, |
| outputs=[red_boss_input_section] |
| ) |
|
|
| cancel_blue_boss_btn.click( |
| fn=hide_blue_boss_input, |
| outputs=[blue_boss_input_section] |
| ) |
|
|
| |
| save_red_boss_btn.click( |
| fn=lambda name, players, board, is_human_playing: update_boss_player("red", name, players, board, is_human_playing), |
| inputs=[red_boss_name_input, players_state, board_state, is_human_playing_state], |
| outputs=[team_html, players_state, red_boss_input_section, play_as_boss_section, input_game_values, is_human_playing_state, empty_red] |
| ).then( |
| fn=lambda: "", |
| outputs=[red_boss_name_input] |
| ) |
|
|
| save_blue_boss_btn.click( |
| fn=lambda name, players, board, is_human_playing: update_boss_player("blue", name, players, board, is_human_playing), |
| inputs=[blue_boss_name_input, players_state, board_state, is_human_playing_state], |
| outputs=[team_html, players_state, blue_boss_input_section, play_as_boss_section, input_game_values, is_human_playing_state, empty_blue] |
| ).then( |
| fn=lambda: "", |
| outputs=[blue_boss_name_input] |
| ) |
|
|
| send_btn.click( |
| fn=lambda: gr.Button(interactive=False), |
| outputs=[send_btn], |
| js=JS_BTN |
| ).then( |
| fn=process_message, |
| inputs=[user_input, messages_state, players_state, board_state, dropdown, guessed_words_state, chat_history_state, is_human_playing_state, turn_state, graph_instance], |
| outputs=[message_feed, guessed_words_state, messages_state, board_state, chat_history_state, winners_state] |
| ).then( |
| fn=lambda: "", |
| outputs=[user_input] |
| ).then( |
| fn=lambda turn: turn + 1, |
| inputs=[turn_state], |
| outputs=[turn_state] |
| ).then( |
| fn=lambda: gr.Button(interactive=True), |
| outputs=[send_btn] |
| ) |
|
|
| guessed_words_state.change( |
| fn=update_plot, |
| inputs=[guessed_words_state, board_state, game_state], |
| outputs=[board_plot] |
| ) |
|
|
| winners_state.change( |
| fn=send_to_database, |
| inputs=[game_state, winners_state] |
| ).then( |
| fn=deactivate_send, |
| inputs=[game_state, winners_state], |
| outputs=[send_btn] |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|